From daaade185e84f714123af3649a17744a90fb208d Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Nov 2020 19:23:41 +1100 Subject: [PATCH 01/25] Removes UmbracoTreeAuthorizeAttribute and migrates usages to authz policies --- .../Authorization/AuthorizationPolicies.cs | 32 +++++++ .../Authorization/TreeAliasesRequirement.cs | 19 +++++ .../UmbracoTreeAuthorizeHandler.cs | 76 +++++++++++++++++ .../Controllers/ContentTypeController.cs | 23 ++--- .../Controllers/DataTypeController.cs | 14 +-- .../Controllers/DictionaryController.cs | 4 +- .../Controllers/LanguageController.cs | 6 +- .../Controllers/MacrosController.cs | 4 +- .../Controllers/MediaTypeController.cs | 16 ++-- .../Controllers/MemberGroupController.cs | 4 +- .../Controllers/MemberTypeController.cs | 13 +-- .../Controllers/RelationTypeController.cs | 4 +- .../Controllers/TemplateController.cs | 4 +- .../BackOfficeServiceCollectionExtensions.cs | 83 +++++++++++++++++- .../Filters/UmbracoTreeAuthorizeAttribute.cs | 85 ------------------- .../Trees/ContentTypeTreeController.cs | 4 +- .../Trees/DataTypeTreeController.cs | 4 +- .../Trees/DictionaryTreeController.cs | 11 ++- .../Trees/LanguageTreeController.cs | 4 +- .../Trees/LogViewerTreeController.cs | 6 +- .../Trees/MacrosTreeController.cs | 4 +- .../Trees/MediaTypeTreeController.cs | 4 +- .../Trees/MemberGroupTreeController.cs | 4 +- .../Trees/MemberTypeTreeController.cs | 4 +- .../Trees/PackagesTreeController.cs | 6 +- .../Trees/PartialViewMacrosTreeController.cs | 6 +- .../Trees/PartialViewsTreeController.cs | 6 +- .../Trees/RelationTypeTreeController.cs | 4 +- .../Trees/TemplatesTreeController.cs | 4 +- .../Trees/UserTreeController.cs | 14 +-- 30 files changed, 308 insertions(+), 164 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/TreeAliasesRequirement.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/UmbracoTreeAuthorizeHandler.cs delete mode 100644 src/Umbraco.Web.BackOffice/Filters/UmbracoTreeAuthorizeAttribute.cs diff --git a/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs new file mode 100644 index 0000000000..7352a43b47 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs @@ -0,0 +1,32 @@ +namespace Umbraco.Web.BackOffice.Authorization +{ + public static class AuthorizationPolicies + { + public const string TreeAccessUsers = "TreeAccessUsers"; + public const string TreeAccessPartialViews = "TreeAccessPartialViews"; + public const string TreeAccessPartialViewMacros = "TreeAccessPartialViewMacros"; + public const string TreeAccessDataTypes = "TreeAccessDataTypes"; + public const string TreeAccessPackages = "TreeAccessPackages"; + public const string TreeAccessLogs = "TreeAccessLogs"; + public const string TreeAccessTemplates = "TreeAccessTemplates"; + public const string TreeAccessDictionary = "TreeAccessDictionary"; + public const string TreeAccessRelationTypes = "TreeAccessRelationTypes"; + public const string TreeAccessMediaTypes = "TreeAccessMediaTypes"; + public const string TreeAccessMacros = "TreeAccessMacros"; + public const string TreeAccessLanguages = "TreeAccessLanguages"; + public const string TreeAccessMemberGroups = "TreeAccessMemberGroups"; + public const string TreeAccessDocumentTypes = "TreeAccessDocumentTypes"; + public const string TreeAccessMemberTypes = "TreeAccessMemberTypes"; + public const string TreeAccessDocumentsOrDocumentTypes = "TreeAccessDocumentsAndDocumentTypes"; + public const string TreeAccessMediaOrMediaTypes = "TreeAccessMediaAndMediaTypes"; + public const string TreeAccessMembersOrMemberTypes = "TreeAccessMembersAndMemberTypes"; + public const string TreeAccessAnySchemaTypes = "TreeAccessSchemaTypes"; + public const string TreeAccessDictionaryOrTemplates = "TreeAccessDictionaryOrTemplates"; + + /// + /// Defines access based on if the user has access to any tree's exposing any types of content (documents, media, members) + /// or any content types (document types, media types, member types) + /// + public const string TreeAccessAnyContentOrTypes = "TreeAccessAnyContentOrTypes"; + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/TreeAliasesRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/TreeAliasesRequirement.cs new file mode 100644 index 0000000000..44ecb8dfe3 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/TreeAliasesRequirement.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Authorization; +using System.Collections.Generic; + +namespace Umbraco.Web.BackOffice.Authorization +{ + + /// + /// Authorization requirements for + /// + public class TreeAliasesRequirement : IAuthorizationRequirement + { + /// + /// The aliases for trees that the user will need access to + /// + public IReadOnlyCollection TreeAliases { get; } + + public TreeAliasesRequirement(params string[] aliases) => TreeAliases = aliases; + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/UmbracoTreeAuthorizeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/UmbracoTreeAuthorizeHandler.cs new file mode 100644 index 0000000000..b2cd18c1ce --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/UmbracoTreeAuthorizeHandler.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Authorization; +using System; +using System.Linq; +using Umbraco.Core; +using System.Threading.Tasks; +using Umbraco.Core.Security; +using Umbraco.Web.Services; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Ensures that the current user has access to the application for which the specified tree(s) belongs + /// + /// + /// This would allow a tree to be moved between sections. + /// The user only needs access to one of the trees specified, not all of the trees. + /// + public class UmbracoTreeAuthorizeHandler : AuthorizationHandler + { + /// + /// Can be used by unit tests to enable/disable this filter + /// + internal static readonly bool Enable = true; + + private readonly ITreeService _treeService; + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + + /// + /// Constructor to set authorization to be based on a tree alias for which application security will be applied + /// + /// + /// + /// + /// If the user has access to the application that the treeAlias is specified in, they will be authorized. + /// Multiple trees may be specified. + /// + public UmbracoTreeAuthorizeHandler(ITreeService treeService, IBackOfficeSecurityAccessor backofficeSecurityAccessor) + { + _treeService = treeService ?? throw new ArgumentNullException(nameof(treeService)); + _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); + } + + private bool IsAuthorized(TreeAliasesRequirement requirement) + { + if (Enable == false) + { + return true; + } + + var apps = requirement.TreeAliases.Select(x => _treeService + .GetByAlias(x)) + .WhereNotNull() + .Select(x => x.SectionAlias) + .Distinct() + .ToArray(); + + return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null + && apps.Any(app => _backofficeSecurityAccessor.BackOfficeSecurity.UserHasSectionAccess( + app, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser)); + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TreeAliasesRequirement requirement) + { + if (IsAuthorized(requirement)) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index 9d18727c62..33f19141c3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -36,6 +36,8 @@ using ContentType = Umbraco.Core.Models.ContentType; using Umbraco.Core.Configuration.Models; using Microsoft.Extensions.Options; using Umbraco.Core.Serialization; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.BackOffice.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -48,7 +50,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// An API controller used for dealing with content types /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] public class ContentTypeController : ContentTypeControllerBase { private readonly IEntityXmlSerializer _serializer; @@ -136,7 +138,7 @@ namespace Umbraco.Web.BackOffice.Controllers } [HttpGet] - [UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] public bool HasContentNodes(int id) { return _contentTypeService.HasContentNodes(id); @@ -223,10 +225,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Gets all user defined properties. /// /// - [UmbracoTreeAuthorize( - Constants.Trees.DocumentTypes, Constants.Trees.Content, - Constants.Trees.MediaTypes, Constants.Trees.Media, - Constants.Trees.MemberTypes, Constants.Trees.Members)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessAnyContentOrTypes)] public IEnumerable GetAllPropertyTypeAliases() { return _contentTypeService.GetAllPropertyTypeAliases(); @@ -236,10 +235,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Gets all the standard fields. /// /// - [UmbracoTreeAuthorize( - Constants.Trees.DocumentTypes, Constants.Trees.Content, - Constants.Trees.MediaTypes, Constants.Trees.Media, - Constants.Trees.MemberTypes, Constants.Trees.Members)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessAnyContentOrTypes)] public IEnumerable GetAllStandardFields() { string[] preValuesSource = { "createDate", "creatorName", "level", "nodeType", "nodeTypeAlias", "pageID", "pageName", "parentID", "path", "template", "updateDate", "writerID", "writerName" }; @@ -280,10 +276,7 @@ namespace Umbraco.Web.BackOffice.Controllers return Ok(result); } - [UmbracoTreeAuthorize( - Constants.Trees.DocumentTypes, Constants.Trees.Content, - Constants.Trees.MediaTypes, Constants.Trees.Media, - Constants.Trees.MemberTypes, Constants.Trees.Members)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessAnyContentOrTypes)] public ContentPropertyDisplay GetPropertyTypeScaffold(int id) { var dataTypeDiff = _dataTypeService.GetDataType(id); @@ -523,7 +516,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Returns the allowed child content type objects for the content item id passed in /// /// - [UmbracoTreeAuthorize(Constants.Trees.DocumentTypes, Constants.Trees.Content)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)] public IEnumerable GetAllowedChildren(int contentId) { if (contentId == Constants.System.RecycleBinContent) diff --git a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs index c88d0d16c9..f8bc313bed 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Net.Mime; using System.Text; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Umbraco.Core; @@ -14,6 +15,7 @@ using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Serialization; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; @@ -31,7 +33,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Content Types, Member Types or Media Types ... and of course to Data Types /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoTreeAuthorize(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)] public class DataTypeController : BackOfficeNotificationsController { private readonly PropertyEditorCollection _propertyEditors; @@ -414,7 +416,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + [UmbracoApplicationAuthorize(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings, Constants.Applications.Packages)] public IEnumerable GetAll() { @@ -430,7 +432,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoTreeAuthorize(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + [UmbracoApplicationAuthorize(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings, Constants.Applications.Packages)] public IDictionary> GetGroupedDataTypes() { @@ -462,9 +464,8 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoTreeAuthorize(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + [UmbracoApplicationAuthorize(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings, Constants.Applications.Packages)] - public IDictionary> GetGroupedPropertyEditors() { var datatypes = new List(); @@ -495,9 +496,8 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoTreeAuthorize(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + [UmbracoApplicationAuthorize(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings, Constants.Applications.Packages)] - public IEnumerable GetAllPropertyEditors() { return _propertyEditorCollection diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index 785264d816..00e10852c7 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -18,6 +18,8 @@ using Umbraco.Web.Security; using Constants = Umbraco.Core.Constants; using Umbraco.Core.Configuration.Models; using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.BackOffice.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -30,7 +32,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Dictionary /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoTreeAuthorize(Constants.Trees.Dictionary)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDictionary)] public class DictionaryController : BackOfficeNotificationsController { private readonly ILogger _logger; diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 66f8b6d7e0..a3e6c83ae6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Umbraco.Core; @@ -10,6 +11,7 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; @@ -80,7 +82,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Deletes a language with a given ID /// - [UmbracoTreeAuthorize(Constants.Trees.Languages)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessLanguages)] [HttpDelete] [HttpPost] public IActionResult DeleteLanguage(int id) @@ -109,7 +111,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Creates or saves a language /// - [UmbracoTreeAuthorize(Constants.Trees.Languages)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessLanguages)] [HttpPost] public Language SaveLanguage(Language language) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs index e4ef90b30c..8b9fb8e7d2 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs @@ -19,6 +19,8 @@ using Umbraco.Web.Security; using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Security; +using Umbraco.Web.BackOffice.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -27,7 +29,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The API controller used for editing dictionary items /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoTreeAuthorize(Constants.Trees.Macros)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMacros)] public class MacrosController : BackOfficeNotificationsController { private readonly ParameterEditorCollection _parameterEditorCollection; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs index cd834e79fb..84a3e5a260 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Core; using Umbraco.Core.Dictionary; @@ -10,6 +11,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Strings; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; @@ -27,7 +29,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// An API controller used for dealing with content types /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoTreeAuthorize(Constants.Trees.MediaTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)] public class MediaTypeController : ContentTypeControllerBase { private readonly IContentTypeService _contentTypeService; @@ -78,7 +80,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [DetermineAmbiguousActionByPassingParameters] - [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaOrMediaTypes)] public MediaTypeDisplay GetById(int id) { var ct = _mediaTypeService.Get(id); @@ -97,7 +99,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [DetermineAmbiguousActionByPassingParameters] - [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaOrMediaTypes)] public MediaTypeDisplay GetById(Guid id) { var mediaType = _mediaTypeService.Get(id); @@ -116,7 +118,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [DetermineAmbiguousActionByPassingParameters] - [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaOrMediaTypes)] public MediaTypeDisplay GetById(Udi id) { var guidUdi = id as GuidUdi; @@ -314,7 +316,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Returns the allowed child content type objects for the content item id passed in - based on an INT id /// /// - [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaOrMediaTypes)] [DetermineAmbiguousActionByPassingParameters] public IEnumerable GetAllowedChildren(int contentId) { @@ -361,7 +363,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Returns the allowed child content type objects for the content item id passed in - based on a GUID id /// /// - [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaOrMediaTypes)] [DetermineAmbiguousActionByPassingParameters] public IEnumerable GetAllowedChildren(Guid contentId) { @@ -378,7 +380,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Returns the allowed child content type objects for the content item id passed in - based on a UDI id /// /// - [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaOrMediaTypes)] [DetermineAmbiguousActionByPassingParameters] public IEnumerable GetAllowedChildren(Udi contentId) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs index d3d06132d3..12f26f484b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs @@ -2,11 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; @@ -19,7 +21,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// An API controller used for dealing with member groups /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoTreeAuthorize(Constants.Trees.MemberGroups)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMemberGroups)] public class MemberGroupController : UmbracoAuthorizedJsonController { private readonly IMemberGroupService _memberGroupService; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs index 3557680ab1..dccc2b7907 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs @@ -24,6 +24,8 @@ using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Editors; using Umbraco.Web.Routing; using Umbraco.Web.Security; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.BackOffice.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -31,7 +33,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// An API controller used for dealing with member types /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoTreeAuthorize(new string[] { Constants.Trees.MemberTypes, Constants.Trees.Members})] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)] public class MemberTypeController : ContentTypeControllerBase { private readonly IMemberTypeService _memberTypeService; @@ -71,7 +73,6 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] [DetermineAmbiguousActionByPassingParameters] public MemberTypeDisplay GetById(int id) { @@ -90,7 +91,6 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] [DetermineAmbiguousActionByPassingParameters] public MemberTypeDisplay GetById(Guid id) { @@ -109,7 +109,6 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] [DetermineAmbiguousActionByPassingParameters] public MemberTypeDisplay GetById(Udi id) { @@ -134,7 +133,6 @@ namespace Umbraco.Web.BackOffice.Controllers /// [HttpDelete] [HttpPost] - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public IActionResult DeleteById(int id) { var foundType = _memberTypeService.Get(id); @@ -161,8 +159,6 @@ namespace Umbraco.Web.BackOffice.Controllers /// be looked up via the db, they need to be passed in. /// /// - - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public IActionResult GetAvailableCompositeMemberTypes(int contentTypeId, [FromQuery]string[] filterContentTypes, [FromQuery]string[] filterPropertyTypes) @@ -176,7 +172,6 @@ namespace Umbraco.Web.BackOffice.Controllers return Ok(result); } - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public MemberTypeDisplay GetEmpty() { var ct = new MemberType(_shortStringHelper, -1); @@ -190,13 +185,13 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Returns all member types /// + [Authorize(Policy = AuthorizationPolicies.TreeAccessMembersOrMemberTypes)] public IEnumerable GetAllTypes() { return _memberTypeService.GetAll() .Select(_umbracoMapper.Map); } - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public ActionResult PostSave(MemberTypeSave contentTypeSave) { //get the persisted member type diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs index 1ab5bd9bfa..7c80ebcc81 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs @@ -15,6 +15,8 @@ using Umbraco.Core.Mapping; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.BackOffice.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -22,7 +24,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The API controller for editing relation types. /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoTreeAuthorize(Constants.Trees.RelationTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessRelationTypes)] public class RelationTypeController : BackOfficeNotificationsController { private readonly ILogger _logger; diff --git a/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs b/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs index f80c3015e9..d60e87a23b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Core; using Umbraco.Core.IO; @@ -9,6 +10,7 @@ using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Strings; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; @@ -18,7 +20,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Controllers { [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoTreeAuthorize(Constants.Trees.Templates)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessTemplates)] public class TemplateController : BackOfficeNotificationsController { private readonly IFileService _fileService; diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 93c6576d4e..7a77739342 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; @@ -9,6 +10,7 @@ using Umbraco.Core.Security; using Umbraco.Core.Serialization; using Umbraco.Infrastructure.BackOffice; using Umbraco.Net; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.AspNetCore; @@ -42,6 +44,8 @@ namespace Umbraco.Extensions // TODO: Need to add more cookie options, see https://github.com/dotnet/aspnetcore/blob/3.0/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs#L45 services.ConfigureOptions(); + + services.AddBackOfficeAuthorizationPolicies(); } public static void AddUmbracoPreview(this IServiceCollection services) @@ -87,7 +91,7 @@ namespace Umbraco.Extensions services.GetRequiredService())); services.TryAddScoped, DefaultUserConfirmation>(); services.TryAddScoped, UserClaimsPrincipalFactory>(); - + // CUSTOM: services.TryAddScoped(); services.TryAddScoped(); @@ -104,5 +108,82 @@ namespace Umbraco.Extensions return new BackOfficeIdentityBuilder(services); } + + private static void AddBackOfficeAuthorizationPolicies(this IServiceCollection services) + { + services.AddAuthorization(options => + { + options.AddPolicy(AuthorizationPolicies.TreeAccessUsers, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Users))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.PartialViews))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViewMacros, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.PartialViewMacros))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessPackages, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Packages))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessLogs, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.LogViewer))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DataTypes))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessTemplates, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Templates))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberTypes))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.RelationTypes))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DocumentTypes))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberGroups))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MediaTypes))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMacros, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Macros))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessLanguages, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Languages))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDictionary, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary, Constants.Trees.Dictionary))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary, Constants.Trees.Templates))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DocumentTypes, Constants.Trees.Content))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MediaTypes, Constants.Trees.Media))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMembersOrMemberTypes, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberTypes, Constants.Trees.Members))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessAnySchemaTypes, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessAnyContentOrTypes, policy => + policy.Requirements.Add(new TreeAliasesRequirement( + Constants.Trees.DocumentTypes, Constants.Trees.Content, + Constants.Trees.MediaTypes, Constants.Trees.Media, + Constants.Trees.MemberTypes, Constants.Trees.Members))); + }); + + services.AddSingleton(); + } } } diff --git a/src/Umbraco.Web.BackOffice/Filters/UmbracoTreeAuthorizeAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/UmbracoTreeAuthorizeAttribute.cs deleted file mode 100644 index eef7469322..0000000000 --- a/src/Umbraco.Web.BackOffice/Filters/UmbracoTreeAuthorizeAttribute.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Linq; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Umbraco.Core; -using Umbraco.Core.Security; -using Umbraco.Web.Security; -using Umbraco.Web.Services; - -namespace Umbraco.Web.BackOffice.Filters -{ - /// - /// Ensures that the current user has access to the application for which the specified tree(s) belongs - /// - /// - /// This would allow a tree to be moved between sections - /// - public class UmbracoTreeAuthorizeAttribute : TypeFilterAttribute - { - public UmbracoTreeAuthorizeAttribute(params string[] treeAliases) : base(typeof(UmbracoTreeAuthorizeFilter)) - { - Arguments = new object[] - { - treeAliases - }; - } - - private sealed class UmbracoTreeAuthorizeFilter : IAuthorizationFilter - { - /// - /// Can be used by unit tests to enable/disable this filter - /// - internal static readonly bool Enable = true; - - private readonly string[] _treeAliases; - - private readonly ITreeService _treeService; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - - /// - /// Constructor to set authorization to be based on a tree alias for which application security will be applied - /// - /// - /// - /// - /// If the user has access to the application that the treeAlias is specified in, they will be authorized. - /// Multiple trees may be specified. - /// - public UmbracoTreeAuthorizeFilter(ITreeService treeService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, - params string[] treeAliases) - { - _treeService = treeService ?? throw new ArgumentNullException(nameof(treeService)); - _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); - _treeAliases = treeAliases; - } - - public void OnAuthorization(AuthorizationFilterContext context) - { - if (!IsAuthorized()) - { - context.Result = new ForbidResult(); - } - } - - private bool IsAuthorized() - { - if (Enable == false) - { - return true; - } - - var apps = _treeAliases.Select(x => _treeService - .GetByAlias(x)) - .WhereNotNull() - .Select(x => x.SectionAlias) - .Distinct() - .ToArray(); - - return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null - && apps.Any(app => _backofficeSecurityAccessor.BackOfficeSecurity.UserHasSectionAccess( - app, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser)); - } - } - } -} diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs index 5c8312c058..15c206e65f 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Actions; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.ContentEditing; @@ -16,7 +18,7 @@ using Umbraco.Web.WebApi; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [Tree(Constants.Applications.Settings, Constants.Trees.DocumentTypes, SortOrder = 0, TreeGroup = Constants.Trees.Groups.Settings)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs index 3502d7a506..f4e42729f0 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs @@ -14,10 +14,12 @@ using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; +using Umbraco.Web.BackOffice.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize(Constants.Trees.DataTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)] [Tree(Constants.Applications.Settings, Constants.Trees.DataTypes, SortOrder = 3, TreeGroup = Constants.Trees.Groups.Settings)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs index a9878a3dbc..163e1607a7 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs @@ -1,10 +1,12 @@ using System; using System.Linq; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Actions; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.Trees; @@ -14,12 +16,9 @@ using Umbraco.Web.WebApi; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize( - Constants.Trees.Dictionary, - Constants.Trees.Templates - // We are allowed to see the dictionary tree, if we are allowed to manage templates, such that se can use the - // dictionary items in templates, even when we dont have authorization to manage the dictionary items - )] + // We are allowed to see the dictionary tree, if we are allowed to manage templates, such that se can use the + // dictionary items in templates, even when we dont have authorization to manage the dictionary items + [Authorize(Policy = AuthorizationPolicies.TreeAccessDictionaryOrTemplates)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] [Tree(Constants.Applications.Translation, Constants.Trees.Dictionary, TreeGroup = Constants.Trees.Groups.Settings)] diff --git a/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs index d4a2c91fad..9ac3d0d43c 100644 --- a/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs @@ -1,5 +1,7 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.Trees; @@ -9,7 +11,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize(Constants.Trees.Languages)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessLanguages)] [Tree(Constants.Applications.Settings, Constants.Trees.Languages, SortOrder = 11, TreeGroup = Constants.Trees.Groups.Settings)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs index 9ffe6e8b68..4e484c447c 100644 --- a/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs @@ -1,5 +1,7 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.Trees; @@ -9,7 +11,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize(Constants.Trees.LogViewer)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessLogs)] [Tree(Constants.Applications.Settings, Constants.Trees.LogViewer, SortOrder= 9, TreeGroup = Constants.Trees.Groups.Settings)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs index 567f0a3646..1df741ce81 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs @@ -8,10 +8,12 @@ using Umbraco.Web.Common.Attributes; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Constants = Umbraco.Core.Constants; +using Umbraco.Web.BackOffice.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize(Constants.Trees.Macros)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMacros)] [Tree(Constants.Applications.Settings, Constants.Trees.Macros, TreeTitle = "Macros", SortOrder = 4, TreeGroup = Constants.Trees.Groups.Settings)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs index 4df81b7023..de8e51719d 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs @@ -13,10 +13,12 @@ using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; +using Umbraco.Web.BackOffice.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize(Constants.Trees.MediaTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)] [Tree(Constants.Applications.Settings, Constants.Trees.MediaTypes, SortOrder = 1, TreeGroup = Constants.Trees.Groups.Settings)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs index 17de4ca37d..440a3439f2 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.Trees; @@ -11,7 +13,7 @@ using Umbraco.Web.WebApi; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize(Constants.Trees.MemberGroups)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMemberGroups)] [Tree(Constants.Applications.Members, Constants.Trees.MemberGroups, SortOrder = 1)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs index c9e340617e..9f560700e8 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs @@ -1,9 +1,11 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Trees; using Umbraco.Web.Common.Attributes; @@ -16,7 +18,7 @@ using Umbraco.Web.WebApi; namespace Umbraco.Web.BackOffice.Trees { [CoreTree] - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)] [Tree(Constants.Applications.Settings, Constants.Trees.MemberTypes, SortOrder = 2, TreeGroup = Constants.Trees.Groups.Settings)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] public class MemberTypeTreeController : MemberTypeAndGroupTreeControllerBase, ISearchableTree diff --git a/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs index fc1f3d876f..08051c6ab3 100644 --- a/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs @@ -1,5 +1,7 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.Trees; @@ -9,7 +11,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize(Constants.Trees.Packages)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessPackages)] [Tree(Constants.Applications.Packages, Constants.Trees.Packages, SortOrder = 0, IsSingleNodeTree = true)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/PartialViewMacrosTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/PartialViewMacrosTreeController.cs index 5baeac7d17..c5f95ebc5e 100644 --- a/src/Umbraco.Web.BackOffice/Trees/PartialViewMacrosTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/PartialViewMacrosTreeController.cs @@ -1,5 +1,7 @@ -using Umbraco.Core.IO; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Core.IO; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Trees; @@ -12,7 +14,7 @@ namespace Umbraco.Web.BackOffice.Trees /// Tree for displaying partial view macros in the developer app /// [Tree(Constants.Applications.Settings, Constants.Trees.PartialViewMacros, SortOrder = 8, TreeGroup = Constants.Trees.Groups.Templating)] - [UmbracoTreeAuthorize(Constants.Trees.PartialViewMacros)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessPartialViewMacros)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] public class PartialViewMacrosTreeController : PartialViewsTreeController diff --git a/src/Umbraco.Web.BackOffice/Trees/PartialViewsTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/PartialViewsTreeController.cs index dcb98ac5b4..8fd7d8de6a 100644 --- a/src/Umbraco.Web.BackOffice/Trees/PartialViewsTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/PartialViewsTreeController.cs @@ -1,5 +1,7 @@ -using Umbraco.Core.IO; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Core.IO; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Composing; @@ -14,7 +16,7 @@ namespace Umbraco.Web.BackOffice.Trees /// Tree for displaying partial views in the settings app /// [Tree(Core.Constants.Applications.Settings, Core.Constants.Trees.PartialViews, SortOrder = 7, TreeGroup = Core.Constants.Trees.Groups.Templating)] - [UmbracoTreeAuthorize(Constants.Trees.PartialViews)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessPartialViews)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] public class PartialViewsTreeController : FileSystemTreeController diff --git a/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs index 965e957f02..ceb8b9a032 100644 --- a/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs @@ -9,10 +9,12 @@ using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; +using Umbraco.Web.BackOffice.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize(Constants.Trees.RelationTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessRelationTypes)] [Tree(Constants.Applications.Settings, Constants.Trees.RelationTypes, SortOrder = 5, TreeGroup = Constants.Trees.Groups.Settings)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs index cfe2d57ce5..2054a6b5f4 100644 --- a/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Models; @@ -8,6 +9,7 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Services; using Umbraco.Extensions; using Umbraco.Web.Actions; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.ContentEditing; @@ -19,7 +21,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize(Constants.Trees.Templates)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessTemplates)] [Tree(Constants.Applications.Settings, Constants.Trees.Templates, SortOrder = 6, TreeGroup = Constants.Trees.Groups.Templating)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/UserTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/UserTreeController.cs index feb8d2a9ec..33e943e9b4 100644 --- a/src/Umbraco.Web.BackOffice/Trees/UserTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/UserTreeController.cs @@ -1,22 +1,16 @@ -using Microsoft.AspNetCore.Http; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Mapping; -using Umbraco.Core.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.Trees; -using Umbraco.Web.Routing; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Trees { - [UmbracoTreeAuthorize(Constants.Trees.Users)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessUsers)] [Tree(Constants.Applications.Users, Constants.Trees.Users, SortOrder = 0, IsSingleNodeTree = true)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] From 63fceebca08764360ac8f0999fd3c65a440bd220 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Nov 2020 22:17:42 +1100 Subject: [PATCH 02/25] Removes UmbracoApplicationAuthorizeAttribute and migrates usages to authz policies --- .../ModelsBuilderDashboardController.cs | 4 +- .../Authorization/AuthorizationPolicies.cs | 58 ++++++++++------ .../SectionAliasesRequirement.cs | 18 +++++ .../UmbracoSectionAuthorizeHandler.cs | 47 +++++++++++++ .../UmbracoTreeAuthorizeHandler.cs | 40 +++++------ .../Controllers/CodeFileController.cs | 4 +- .../Controllers/ContentController.cs | 9 +-- .../Controllers/DataTypeController.cs | 12 ++-- .../Controllers/LogController.cs | 6 +- .../Controllers/MediaController.cs | 4 +- .../Controllers/MemberController.cs | 4 +- .../Controllers/PackageController.cs | 4 +- .../Controllers/PackageInstallController.cs | 4 +- .../Controllers/RelationController.cs | 4 +- .../Controllers/TinyMceController.cs | 7 +- .../Controllers/UserGroupsController.cs | 4 +- .../Controllers/UsersController.cs | 4 +- .../BackOfficeServiceCollectionExtensions.cs | 58 +++++++++++++++- .../UmbracoApplicationAuthorizeAttribute.cs | 67 ------------------- .../HealthCheck/HealthCheckController.cs | 4 +- .../Profiling/WebProfilingController.cs | 4 +- .../Trees/ContentBlueprintTreeController.cs | 4 +- .../Trees/ContentTreeController.cs | 15 +---- .../Trees/MediaTreeController.cs | 11 +-- .../Trees/MemberTreeController.cs | 9 +-- 25 files changed, 233 insertions(+), 172 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Authorization/SectionAliasesRequirement.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/UmbracoSectionAuthorizeHandler.cs delete mode 100644 src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs index 3adbc0df2c..76c33e8de4 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.Serialization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Umbraco.Configuration; @@ -7,6 +8,7 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Exceptions; using Umbraco.Core.Hosting; using Umbraco.ModelsBuilder.Embedded.Building; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Filters; @@ -20,7 +22,7 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice /// correct CSRF security is adhered to for angular and it also ensures that this controller is not subseptipal to /// global WebApi formatters being changed since this is always forced to only return Angular JSON Specific formats. /// - [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] public class ModelsBuilderDashboardController : UmbracoAuthorizedJsonController { private readonly ModelsBuilderSettings _config; diff --git a/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs index 7352a43b47..b6cad8dd06 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs @@ -2,31 +2,47 @@ { public static class AuthorizationPolicies { - public const string TreeAccessUsers = "TreeAccessUsers"; - public const string TreeAccessPartialViews = "TreeAccessPartialViews"; - public const string TreeAccessPartialViewMacros = "TreeAccessPartialViewMacros"; - public const string TreeAccessDataTypes = "TreeAccessDataTypes"; - public const string TreeAccessPackages = "TreeAccessPackages"; - public const string TreeAccessLogs = "TreeAccessLogs"; - public const string TreeAccessTemplates = "TreeAccessTemplates"; - public const string TreeAccessDictionary = "TreeAccessDictionary"; - public const string TreeAccessRelationTypes = "TreeAccessRelationTypes"; - public const string TreeAccessMediaTypes = "TreeAccessMediaTypes"; - public const string TreeAccessMacros = "TreeAccessMacros"; - public const string TreeAccessLanguages = "TreeAccessLanguages"; - public const string TreeAccessMemberGroups = "TreeAccessMemberGroups"; - public const string TreeAccessDocumentTypes = "TreeAccessDocumentTypes"; - public const string TreeAccessMemberTypes = "TreeAccessMemberTypes"; - public const string TreeAccessDocumentsOrDocumentTypes = "TreeAccessDocumentsAndDocumentTypes"; - public const string TreeAccessMediaOrMediaTypes = "TreeAccessMediaAndMediaTypes"; - public const string TreeAccessMembersOrMemberTypes = "TreeAccessMembersAndMemberTypes"; - public const string TreeAccessAnySchemaTypes = "TreeAccessSchemaTypes"; - public const string TreeAccessDictionaryOrTemplates = "TreeAccessDictionaryOrTemplates"; + // TODO: Rethink these names! Describe what they are doing probably + + public const string SectionAccessContentOrMedia = nameof(SectionAccessContentOrMedia); + public const string SectionAccessContent = nameof(SectionAccessContent); + public const string SectionAccessPackages = nameof(SectionAccessPackages); + public const string SectionAccessUsers = nameof(SectionAccessUsers); + public const string SectionAccessMedia = nameof(SectionAccessMedia); + public const string SectionAccessSettings = nameof(SectionAccessSettings); + public const string SectionAccessForTinyMce = nameof(SectionAccessForTinyMce); + public const string SectionAccessForMemberTree = nameof(SectionAccessForMemberTree); + public const string SectionAccessForMediaTree = nameof(SectionAccessForMediaTree); + public const string SectionAccessForContentTree = nameof(SectionAccessForContentTree); + public const string SectionAccessForDataTypeReading = nameof(SectionAccessForDataTypeReading); + public const string SectionAccessMembers = nameof(SectionAccessMembers); + + public const string TreeAccessDocuments = nameof(TreeAccessDocuments); + public const string TreeAccessUsers = nameof(TreeAccessUsers); + public const string TreeAccessPartialViews = nameof(TreeAccessPartialViews); + public const string TreeAccessPartialViewMacros = nameof(TreeAccessPartialViewMacros); + public const string TreeAccessDataTypes = nameof(TreeAccessDataTypes); + public const string TreeAccessPackages = nameof(TreeAccessPackages); + public const string TreeAccessLogs = nameof(TreeAccessLogs); + public const string TreeAccessTemplates = nameof(TreeAccessTemplates); + public const string TreeAccessDictionary = nameof(TreeAccessDictionary); + public const string TreeAccessRelationTypes = nameof(TreeAccessRelationTypes); + public const string TreeAccessMediaTypes = nameof(TreeAccessMediaTypes); + public const string TreeAccessMacros = nameof(TreeAccessMacros); + public const string TreeAccessLanguages = nameof(TreeAccessLanguages); + public const string TreeAccessMemberGroups = nameof(TreeAccessMemberGroups); + public const string TreeAccessDocumentTypes = nameof(TreeAccessDocumentTypes); + public const string TreeAccessMemberTypes = nameof(TreeAccessMemberTypes); + public const string TreeAccessDocumentsOrDocumentTypes = nameof(TreeAccessDocumentsOrDocumentTypes); + public const string TreeAccessMediaOrMediaTypes = nameof(TreeAccessMediaOrMediaTypes); + public const string TreeAccessMembersOrMemberTypes = nameof(TreeAccessMembersOrMemberTypes); + public const string TreeAccessAnySchemaTypes = nameof(TreeAccessAnySchemaTypes); + public const string TreeAccessDictionaryOrTemplates = nameof(TreeAccessDictionaryOrTemplates); /// /// Defines access based on if the user has access to any tree's exposing any types of content (documents, media, members) /// or any content types (document types, media types, member types) /// - public const string TreeAccessAnyContentOrTypes = "TreeAccessAnyContentOrTypes"; + public const string TreeAccessAnyContentOrTypes = nameof(TreeAccessAnyContentOrTypes); } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/SectionAliasesRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/SectionAliasesRequirement.cs new file mode 100644 index 0000000000..9789258bf8 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/SectionAliasesRequirement.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authorization; +using System.Collections.Generic; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Authorization requirements for + /// + public class SectionAliasesRequirement : IAuthorizationRequirement + { + /// + /// The aliases for sections that the user will need access to + /// + public IReadOnlyCollection SectionAliases { get; } + + public SectionAliasesRequirement(params string[] aliases) => SectionAliases = aliases; + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/UmbracoSectionAuthorizeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/UmbracoSectionAuthorizeHandler.cs new file mode 100644 index 0000000000..b063c4c7d9 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/UmbracoSectionAuthorizeHandler.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Authorization; +using System.Linq; +using System.Threading.Tasks; +using Umbraco.Core.Security; + +namespace Umbraco.Web.BackOffice.Authorization +{ + + /// + /// Ensures that the current user has access to the section + /// + /// + /// The user only needs access to one of the sections specified, not all of the sections. + /// + public class UmbracoSectionAuthorizeHandler : AuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + + public UmbracoSectionAuthorizeHandler(IBackOfficeSecurityAccessor backofficeSecurityAccessor) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SectionAliasesRequirement requirement) + { + if (IsAuthorized(requirement)) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + + private bool IsAuthorized(SectionAliasesRequirement requirement) + { + var authorized = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null + && requirement.SectionAliases.Any(app => _backofficeSecurityAccessor.BackOfficeSecurity.UserHasSectionAccess( + app, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser)); + + return authorized; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/UmbracoTreeAuthorizeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/UmbracoTreeAuthorizeHandler.cs index b2cd18c1ce..9be8fd578f 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/UmbracoTreeAuthorizeHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/UmbracoTreeAuthorizeHandler.cs @@ -8,8 +8,9 @@ using Umbraco.Web.Services; namespace Umbraco.Web.BackOffice.Authorization { + /// - /// Ensures that the current user has access to the application for which the specified tree(s) belongs + /// Ensures that the current user has access to the section for which the specified tree(s) belongs /// /// /// This would allow a tree to be moved between sections. @@ -17,10 +18,6 @@ namespace Umbraco.Web.BackOffice.Authorization /// public class UmbracoTreeAuthorizeHandler : AuthorizationHandler { - /// - /// Can be used by unit tests to enable/disable this filter - /// - internal static readonly bool Enable = true; private readonly ITreeService _treeService; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; @@ -40,25 +37,6 @@ namespace Umbraco.Web.BackOffice.Authorization _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); } - private bool IsAuthorized(TreeAliasesRequirement requirement) - { - if (Enable == false) - { - return true; - } - - var apps = requirement.TreeAliases.Select(x => _treeService - .GetByAlias(x)) - .WhereNotNull() - .Select(x => x.SectionAlias) - .Distinct() - .ToArray(); - - return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null - && apps.Any(app => _backofficeSecurityAccessor.BackOfficeSecurity.UserHasSectionAccess( - app, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser)); - } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TreeAliasesRequirement requirement) { if (IsAuthorized(requirement)) @@ -72,5 +50,19 @@ namespace Umbraco.Web.BackOffice.Authorization return Task.CompletedTask; } + + private bool IsAuthorized(TreeAliasesRequirement requirement) + { + var apps = requirement.TreeAliases.Select(x => _treeService + .GetByAlias(x)) + .WhereNotNull() + .Select(x => x.SectionAlias) + .Distinct() + .ToArray(); + + return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null + && apps.Any(app => _backofficeSecurityAccessor.BackOfficeSecurity.UserHasSectionAccess( + app, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser)); + } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index 4bf76a108a..118dbf0926 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Umbraco.Core; @@ -14,6 +15,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Core.Strings.Css; using Umbraco.Extensions; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; @@ -28,7 +30,7 @@ namespace Umbraco.Web.BackOffice.Controllers // ref: https://www.exceptionnotfound.net/the-asp-net-web-api-exception-handling-pipeline-a-guided-tour/ [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] //[PrefixlessBodyModelValidator] - [UmbracoApplicationAuthorize(Constants.Applications.Settings)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] public class CodeFileController : BackOfficeNotificationsController { private readonly IIOHelper _ioHelper; diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index ad51ea7dfd..f91bb9c124 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -37,19 +37,16 @@ using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Common.Filters; using Umbraco.Web.Models.Mapping; - +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.BackOffice.Authorization; namespace Umbraco.Web.BackOffice.Controllers { /// /// The API controller used for editing content /// - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the content application. - /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorize(Constants.Applications.Content)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)] public class ContentController : ContentControllerBase { private readonly PropertyEditorCollection _propertyEditors; diff --git a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs index f8bc313bed..eb2108a5e2 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs @@ -416,8 +416,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoApplicationAuthorize(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, - Constants.Applications.Settings, Constants.Applications.Packages)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessForDataTypeReading)] public IEnumerable GetAll() { return _dataTypeService @@ -432,8 +431,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoApplicationAuthorize(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, - Constants.Applications.Settings, Constants.Applications.Packages)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessForDataTypeReading)] public IDictionary> GetGroupedDataTypes() { var dataTypes = _dataTypeService @@ -464,8 +462,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoApplicationAuthorize(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, - Constants.Applications.Settings, Constants.Applications.Packages)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessForDataTypeReading)] public IDictionary> GetGroupedPropertyEditors() { var datatypes = new List(); @@ -496,8 +493,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members /// - [UmbracoApplicationAuthorize(Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, - Constants.Applications.Settings, Constants.Applications.Packages)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessForDataTypeReading)] public IEnumerable GetAllPropertyEditors() { return _propertyEditorCollection diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs index 4d816624de..a8210eb20b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.AspNetCore.Authorization; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; @@ -10,6 +11,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Security; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.ContentEditing; @@ -52,7 +54,7 @@ namespace Umbraco.Web.BackOffice.Controllers _sqlContext = sqlContext ?? throw new ArgumentNullException(nameof(sqlContext)); } - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content, Constants.Applications.Media)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessContentOrMedia)] public PagedResult GetPagedEntityLog(int id, int pageNumber = 1, int pageSize = 10, diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 0e97ed84af..5d8b9c0e97 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -40,6 +40,8 @@ using Umbraco.Web.Common.Exceptions; using Umbraco.Web.ContentApps; using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; +using Umbraco.Web.BackOffice.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -48,7 +50,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// access to ALL of the methods on this controller will need access to the media application. /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorize(Constants.Applications.Media)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessMedia)] public class MediaController : ContentControllerBase { private readonly IShortStringHelper _shortStringHelper; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 55042d458b..0b090a5e47 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Net.Mime; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -24,6 +25,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Extensions; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.ModelBinders; using Umbraco.Web.Common.Attributes; @@ -41,7 +43,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// access to ALL of the methods on this controller will need access to the member application. /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorize(Constants.Applications.Members)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessMembers)] [OutgoingNoHyphenGuidFormat] public class MemberController : ContentControllerBase { diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index 69d23d606b..de855ab421 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Net; using System.Text; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using Semver; @@ -12,6 +13,7 @@ using Umbraco.Core.Hosting; using Umbraco.Core.Models.Packaging; using Umbraco.Core.Security; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; @@ -23,7 +25,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// A controller used for managing packages in the back office /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Packages)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessPackages)] public class PackageController : UmbracoAuthorizedJsonController { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs index 05631173c9..e08dfeff60 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs @@ -22,6 +22,8 @@ using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Security; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.BackOffice.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -29,7 +31,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// A controller used for installing packages and managing all of the data in the packages section in the back office /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Packages)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessPackages)] public class PackageInstallController : UmbracoAuthorizedJsonController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs index bf40e5722f..b8d5e1bc20 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; @@ -18,7 +20,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Controllers { [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessContent)] public class RelationController : UmbracoAuthorizedJsonController { private readonly UmbracoMapper _umbracoMapper; diff --git a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs index 4bae8970bc..8f7b5c5a0e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -14,6 +15,7 @@ using Umbraco.Core.Hosting; using Umbraco.Core.IO; using Umbraco.Core.Media; using Umbraco.Core.Strings; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; @@ -23,10 +25,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Controllers { [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorize( - Constants.Applications.Content, - Constants.Applications.Media, - Constants.Applications.Members)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessForTinyMce)] public class TinyMceController : UmbracoAuthorizedApiController { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs index 4cba5064d1..940f2a76df 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs @@ -16,11 +16,13 @@ using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Security; using Constants = Umbraco.Core.Constants; +using Umbraco.Web.BackOffice.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Umbraco.Web.BackOffice.Controllers { [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorize(Constants.Applications.Users)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)] [PrefixlessBodyModelValidator] public class UserGroupsController : UmbracoAuthorizedJsonController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index f187ec22ce..9da8e592a2 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -42,11 +42,13 @@ using Task = System.Threading.Tasks.Task; using Umbraco.Net; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Security; +using Umbraco.Web.BackOffice.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Umbraco.Web.BackOffice.Controllers { [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorize(Constants.Applications.Users)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)] [PrefixlessBodyModelValidator] [IsCurrentUserModelFilter] public class UsersController : UmbracoAuthorizedJsonController diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 7a77739342..00ab69f50e 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -109,10 +109,66 @@ namespace Umbraco.Extensions return new BackOfficeIdentityBuilder(services); } + /// + /// Add authorization handlers and policies + /// + /// private static void AddBackOfficeAuthorizationPolicies(this IServiceCollection services) { + services.AddSingleton(); + services.AddSingleton(); + services.AddAuthorization(options => { + + options.AddPolicy(AuthorizationPolicies.SectionAccessContent, policy => + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Content))); + + options.AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, policy => + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Content, Constants.Applications.Media))); + + options.AddPolicy(AuthorizationPolicies.SectionAccessUsers, policy => + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Users))); + + options.AddPolicy(AuthorizationPolicies.SectionAccessForTinyMce, policy => + policy.Requirements.Add(new SectionAliasesRequirement( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members))); + + options.AddPolicy(AuthorizationPolicies.SectionAccessMedia, policy => + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Media))); + + options.AddPolicy(AuthorizationPolicies.SectionAccessMembers, policy => + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Members))); + + options.AddPolicy(AuthorizationPolicies.SectionAccessPackages, policy => + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Packages))); + + options.AddPolicy(AuthorizationPolicies.SectionAccessSettings, policy => + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Settings))); + + //We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered + // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. + options.AddPolicy(AuthorizationPolicies.SectionAccessForContentTree, policy => + policy.Requirements.Add(new SectionAliasesRequirement( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, + Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members))); + options.AddPolicy(AuthorizationPolicies.SectionAccessForMediaTree, policy => + policy.Requirements.Add(new SectionAliasesRequirement( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, + Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members))); + options.AddPolicy(AuthorizationPolicies.SectionAccessForMemberTree, policy => + policy.Requirements.Add(new SectionAliasesRequirement( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members))); + + // Permission is granted to this policy if the user has access to any of these sections: Content, media, settings, developer, members + options.AddPolicy(AuthorizationPolicies.SectionAccessForDataTypeReading, policy => + policy.Requirements.Add(new SectionAliasesRequirement( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + Constants.Applications.Settings, Constants.Applications.Packages))); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDocuments, policy => + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Content))); + options.AddPolicy(AuthorizationPolicies.TreeAccessUsers, policy => policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Users))); @@ -182,8 +238,6 @@ namespace Umbraco.Extensions Constants.Trees.MediaTypes, Constants.Trees.Media, Constants.Trees.MemberTypes, Constants.Trees.Members))); }); - - services.AddSingleton(); } } } diff --git a/src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs deleted file mode 100644 index cc1bdd4cd5..0000000000 --- a/src/Umbraco.Web.BackOffice/Filters/UmbracoApplicationAuthorizeAttribute.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Linq; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Umbraco.Core.Security; -using Umbraco.Web.Security; - -namespace Umbraco.Web.BackOffice.Filters -{ - public class UmbracoApplicationAuthorizeAttribute : TypeFilterAttribute - { - public UmbracoApplicationAuthorizeAttribute(params string[] appName) : base(typeof(UmbracoApplicationAuthorizeFilter)) - { - base.Arguments = new object[] - { - appName - }; - } - - private class UmbracoApplicationAuthorizeFilter : IAuthorizationFilter - { - /// - /// Can be used by unit tests to enable/disable this filter - /// - internal static bool Enable = true; - - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - private readonly string[] _appNames; - - /// - /// Constructor to set any number of applications that the user needs access to be authorized - /// - /// - /// - /// If the user has access to any of the specified apps, they will be authorized. - /// - public UmbracoApplicationAuthorizeFilter(IBackOfficeSecurityAccessor backofficeSecurityAccessor, params string[] appName) - { - _backofficeSecurityAccessor = backofficeSecurityAccessor; - _appNames = appName; - } - - - public void OnAuthorization(AuthorizationFilterContext context) - { - if (!IsAuthorized()) - { - context.Result = new ForbidResult(); - } - } - - private bool IsAuthorized() - { - if (Enable == false) - { - return true; - } - - var authorized = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null - && _appNames.Any(app => _backofficeSecurityAccessor.BackOfficeSecurity.UserHasSectionAccess( - app, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser)); - - return authorized; - } - } - } - -} diff --git a/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs b/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs index 768ede1787..44d8161519 100644 --- a/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs +++ b/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs @@ -10,6 +10,8 @@ using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Microsoft.Extensions.Logging; +using Umbraco.Web.BackOffice.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Umbraco.Web.BackOffice.HealthCheck { @@ -17,7 +19,7 @@ namespace Umbraco.Web.BackOffice.HealthCheck /// The API controller used to display the health check info and execute any actions /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorize(Constants.Applications.Settings)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] public class HealthCheckController : UmbracoAuthorizedJsonController { private readonly HealthCheckCollection _checks; diff --git a/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs b/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs index 101e00d752..ae936984b0 100644 --- a/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs +++ b/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs @@ -4,6 +4,8 @@ using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Controllers; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.BackOffice.Authorization; namespace Umbraco.Web.BackOffice.Profiling { @@ -11,7 +13,7 @@ namespace Umbraco.Web.BackOffice.Profiling /// The API controller used to display the state of the web profiler /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorize(Constants.Applications.Settings)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] public class WebProfilingController : UmbracoAuthorizedJsonController { private readonly IHostingEnvironment _hosting; diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs index 5477797ccc..eba16f288f 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs @@ -1,11 +1,13 @@ using System; using System.Linq; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Services; using Umbraco.Web.Actions; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.Trees; @@ -21,7 +23,7 @@ namespace Umbraco.Web.BackOffice.Trees /// /// This authorizes based on access to the content section even though it exists in the settings /// - [UmbracoApplicationAuthorize(Constants.Applications.Content)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessContent)] [Tree(Constants.Applications.Settings, Constants.Trees.ContentBlueprints, SortOrder = 12, TreeGroup = Constants.Trees.Groups.Settings)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs index 755acdf0cb..6bc19e058c 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs @@ -12,29 +12,20 @@ using Umbraco.Web.Actions; using Umbraco.Web.Models.Trees; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Search; -using Umbraco.Core.Configuration; using Umbraco.Core.Security; using Constants = Umbraco.Core.Constants; -using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; -using Umbraco.Web.Security; using Umbraco.Web.WebApi; using Umbraco.Core.Configuration.Models; using Microsoft.Extensions.Options; using Umbraco.Web.Trees; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.BackOffice.Authorization; namespace Umbraco.Web.BackOffice.Trees { - //We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered - // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. - [UmbracoApplicationAuthorize( - Constants.Applications.Content, - Constants.Applications.Media, - Constants.Applications.Users, - Constants.Applications.Settings, - Constants.Applications.Packages, - Constants.Applications.Members)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessForContentTree)] [Tree(Constants.Applications.Content, Constants.Trees.Content)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs index ab332e3843..2ae60047a3 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs @@ -20,17 +20,12 @@ using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Security; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.BackOffice.Authorization; namespace Umbraco.Web.BackOffice.Trees { - //We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered - // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. - [UmbracoApplicationAuthorize( - Constants.Applications.Content, - Constants.Applications.Media, - Constants.Applications.Settings, - Constants.Applications.Packages, - Constants.Applications.Members)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessForMediaTree)] [Tree(Constants.Applications.Media, Constants.Trees.Media)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs index 31ab66908c..e1b898c142 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs @@ -20,15 +20,12 @@ using Constants = Umbraco.Core.Constants; using Umbraco.Web.Security; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; +using Umbraco.Web.BackOffice.Authorization; +using Microsoft.AspNetCore.Authorization; namespace Umbraco.Web.BackOffice.Trees { - //We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered - // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. - [UmbracoApplicationAuthorize( - Constants.Applications.Content, - Constants.Applications.Media, - Constants.Applications.Members)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessForMemberTree)] [Tree(Constants.Applications.Members, Constants.Trees.Members, SortOrder = 0)] [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] From 29c2bca0121225fe9a678f0afc28b6a2be01b63b Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Nov 2020 22:18:56 +1100 Subject: [PATCH 03/25] remove delete relation in relation controller, this is not used and shouldn't really be there if only having access to content. --- .../Controllers/RelationController.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs index b8d5e1bc20..e9812aa215 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs @@ -58,20 +58,5 @@ namespace Umbraco.Web.BackOffice.Controllers return _umbracoMapper.MapEnumerable(relations); } - [HttpDelete] - [HttpPost] - public IActionResult DeleteById(int id) - { - var foundRelation = _relationService.GetById(id); - - if (foundRelation == null) - { - return new UmbracoProblemResult("No relation found with the specified id", HttpStatusCode.NotFound); - } - - _relationService.Delete(foundRelation); - - return Ok(); - } } } From 6a995127b16de94106c65e736d66f8586d9f079a Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Nov 2020 23:53:04 +1100 Subject: [PATCH 04/25] Migrates AdminUsersAuthorizeAttribute and DenyLocalLoginAuthorizationAttribute to authz policies --- .../Editors/UserEditorAuthorizationHelper.cs | 4 +- .../Runtime/CoreInitialComposer.cs | 2 + .../UserEditorAuthorizationHelperTests.cs | 12 --- .../AdminUsersAuthorizeHandler.cs | 73 ++++++++++++++ .../AdminUsersAuthorizeRequirement.cs | 17 ++++ .../Authorization/AuthorizationPolicies.cs | 22 ++++- .../DenyLocalLoginAuthorizeHandler.cs | 34 +++++++ .../DenyLocalLoginRequirement.cs | 11 +++ .../Controllers/AuthenticationController.cs | 11 ++- .../Controllers/UsersController.cs | 41 ++++---- .../BackOfficeServiceCollectionExtensions.cs | 16 ++++ .../Filters/AdminUsersAuthorizeAttribute.cs | 96 ------------------- .../DenyLocalLoginAuthorizationAttribute.cs | 34 ------- 13 files changed, 194 insertions(+), 179 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeRequirement.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginAuthorizeHandler.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginRequirement.cs delete mode 100644 src/Umbraco.Web.BackOffice/Filters/AdminUsersAuthorizeAttribute.cs delete mode 100644 src/Umbraco.Web.BackOffice/Filters/DenyLocalLoginAuthorizationAttribute.cs diff --git a/src/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelper.cs b/src/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelper.cs index 63b5cc90dc..d19f4d954c 100644 --- a/src/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelper.cs +++ b/src/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelper.cs @@ -12,14 +12,12 @@ namespace Umbraco.Web.Editors { private readonly IContentService _contentService; private readonly IMediaService _mediaService; - private readonly IUserService _userService; private readonly IEntityService _entityService; - public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IUserService userService, IEntityService entityService) + public UserEditorAuthorizationHelper(IContentService contentService, IMediaService mediaService, IEntityService entityService) { _contentService = contentService; _mediaService = mediaService; - _userService = userService; _entityService = entityService; } diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index 0bf153dff5..0328e782f8 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -375,6 +375,8 @@ namespace Umbraco.Core.Runtime composition.Services.AddUnique(); composition.Services.AddUnique(); + + composition.Services.AddUnique(); } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs index 642f9d335a..2bf9a541b9 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs @@ -30,7 +30,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new string[0]); @@ -52,7 +51,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new string[0]); @@ -74,7 +72,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new[] {"FunGroup"}); @@ -96,7 +93,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); var result = authHelper.IsAuthorized(currentUser, savingUser, new int[0], new int[0], new[] { "test" }); @@ -133,7 +129,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); //adding 5555 which currentUser has access to since it's a child of 9876 ... adding is still ok even though currentUser doesn't have access to 1234 @@ -171,7 +166,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); //removing 4567 start node even though currentUser doesn't have acces to it ... removing is ok @@ -209,7 +203,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); //adding 1234 but currentUser doesn't have access to it ... nope @@ -247,7 +240,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); //adding 5555 which currentUser has access to since it's a child of 9876 ... ok @@ -286,7 +278,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); //adding 1234 but currentUser doesn't have access to it ... nope @@ -324,7 +315,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); //adding 5555 which currentUser has access to since it's a child of 9876 ... ok @@ -362,7 +352,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); //adding 5555 which currentUser has access to since it's a child of 9876 ... adding is still ok even though currentUser doesn't have access to 1234 @@ -400,7 +389,6 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.Editors var authHelper = new UserEditorAuthorizationHelper( contentService.Object, mediaService.Object, - userService.Object, entityService.Object); //removing 4567 start node even though currentUser doesn't have acces to it ... removing is ok diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs new file mode 100644 index 0000000000..30f9502dde --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using System.Linq; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Security; +using Umbraco.Core.Services; +using Umbraco.Web.Editors; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// if the users being edited is an admin then we must ensure that the current user is also an admin + /// + public class AdminUsersAuthorizeHandler : AuthorizationHandler + { + private readonly IHttpContextAccessor _httpContextAcessor; + private readonly IUserService _userService; + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly UserEditorAuthorizationHelper _userEditorAuthorizationHelper; + + public AdminUsersAuthorizeHandler(IHttpContextAccessor httpContextAcessor, + IUserService userService, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + UserEditorAuthorizationHelper userEditorAuthorizationHelper) + { + _httpContextAcessor = httpContextAcessor; + _userService = userService; + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _userEditorAuthorizationHelper = userEditorAuthorizationHelper; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminUsersAuthorizeRequirement requirement) + { + var isAuth = IsAuthorized(context, requirement); + if (!isAuth.HasValue || isAuth.Value) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + + private bool? IsAuthorized(AuthorizationHandlerContext context, AdminUsersAuthorizeRequirement requirement) + { + int[] userIds; + + var queryString = _httpContextAcessor.HttpContext?.Request.Query[requirement.QueryStringName]; + if (!queryString.HasValue) return null; + + if (int.TryParse(queryString, out var userId)) + { + userIds = new[] { userId }; + } + else + { + var ids = _httpContextAcessor.HttpContext.Request.Query.Where(x => x.Key == requirement.QueryStringName).ToArray(); + if (ids.Length == 0) + return null; + userIds = ids.Select(x => x.Value.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); + } + + if (userIds.Length == 0) return null; + + var users = _userService.GetUsersById(userIds); + return users.All(user => _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, null) != false); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeRequirement.cs new file mode 100644 index 0000000000..5b150db080 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeRequirement.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Authorization requirement for the + /// + public class AdminUsersAuthorizeRequirement : IAuthorizationRequirement + { + public AdminUsersAuthorizeRequirement(string queryStringName = "id") + { + QueryStringName = queryStringName; + } + + public string QueryStringName { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs index b6cad8dd06..bcf68688d5 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs @@ -1,21 +1,32 @@ namespace Umbraco.Web.BackOffice.Authorization { + /// + /// A list of authorization policy names for use in the back office + /// public static class AuthorizationPolicies { - // TODO: Rethink these names! Describe what they are doing probably + public const string AdminUserEditsRequireAdmin = nameof(AdminUserEditsRequireAdmin); + public const string DenyLocalLoginIfConfigured = nameof(DenyLocalLoginIfConfigured); + + // Single section access - public const string SectionAccessContentOrMedia = nameof(SectionAccessContentOrMedia); public const string SectionAccessContent = nameof(SectionAccessContent); public const string SectionAccessPackages = nameof(SectionAccessPackages); public const string SectionAccessUsers = nameof(SectionAccessUsers); public const string SectionAccessMedia = nameof(SectionAccessMedia); public const string SectionAccessSettings = nameof(SectionAccessSettings); + public const string SectionAccessMembers = nameof(SectionAccessMembers); + + // Custom access based on multiple sections + + public const string SectionAccessContentOrMedia = nameof(SectionAccessContentOrMedia); public const string SectionAccessForTinyMce = nameof(SectionAccessForTinyMce); public const string SectionAccessForMemberTree = nameof(SectionAccessForMemberTree); public const string SectionAccessForMediaTree = nameof(SectionAccessForMediaTree); public const string SectionAccessForContentTree = nameof(SectionAccessForContentTree); - public const string SectionAccessForDataTypeReading = nameof(SectionAccessForDataTypeReading); - public const string SectionAccessMembers = nameof(SectionAccessMembers); + public const string SectionAccessForDataTypeReading = nameof(SectionAccessForDataTypeReading); + + // Single tree access public const string TreeAccessDocuments = nameof(TreeAccessDocuments); public const string TreeAccessUsers = nameof(TreeAccessUsers); @@ -33,6 +44,9 @@ public const string TreeAccessMemberGroups = nameof(TreeAccessMemberGroups); public const string TreeAccessDocumentTypes = nameof(TreeAccessDocumentTypes); public const string TreeAccessMemberTypes = nameof(TreeAccessMemberTypes); + + // Custom access based on multiple trees + public const string TreeAccessDocumentsOrDocumentTypes = nameof(TreeAccessDocumentsOrDocumentTypes); public const string TreeAccessMediaOrMediaTypes = nameof(TreeAccessMediaOrMediaTypes); public const string TreeAccessMembersOrMemberTypes = nameof(TreeAccessMembersOrMemberTypes); diff --git a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginAuthorizeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginAuthorizeHandler.cs new file mode 100644 index 0000000000..510682081a --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginAuthorizeHandler.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; +using Umbraco.Web.Common.Security; + +namespace Umbraco.Web.BackOffice.Authorization +{ + + /// + /// Ensures the resource cannot be accessed if returns true + /// + public class DenyLocalLoginAuthorizeHandler : AuthorizationHandler + { + private readonly IBackOfficeExternalLoginProviders _externalLogins; + + public DenyLocalLoginAuthorizeHandler(IBackOfficeExternalLoginProviders externalLogins) + { + _externalLogins = externalLogins; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyLocalLoginRequirement requirement) + { + if (_externalLogins.HasDenyLocalLogin()) + { + context.Fail(); + } + else + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginRequirement.cs new file mode 100644 index 0000000000..5fe7f1c071 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Marker requirement for the + /// + public class DenyLocalLoginRequirement : IAuthorizationRequirement + { + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 4441d1b2f3..98c36044ca 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -30,7 +30,8 @@ using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Security; using Constants = Umbraco.Core.Constants; using Microsoft.AspNetCore.Identity; -using Umbraco.Web.Editors.Filters; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.BackOffice.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -126,7 +127,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// This will also update the security stamp for the user so it can only be used once /// [ValidateAngularAntiForgeryToken] - [DenyLocalLoginAuthorization] + [Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)] public async Task> PostVerifyInvite([FromQuery] int id, [FromQuery] string token) { if (string.IsNullOrWhiteSpace(token)) @@ -265,7 +266,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// [UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: false, requireApproval: false)] [SetAngularAntiForgeryTokens] - [DenyLocalLoginAuthorization] + [Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)] public ActionResult GetCurrentInvitedUser() { var user = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; @@ -289,7 +290,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [SetAngularAntiForgeryTokens] - [DenyLocalLoginAuthorization] + [Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)] public async Task PostLogin(LoginModel loginModel) { // Sign the user in with username/password, this also gives a chance for developers to @@ -356,7 +357,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [SetAngularAntiForgeryTokens] - [DenyLocalLoginAuthorization] + [Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)] public async Task PostRequestPasswordReset(RequestPasswordResetModel model) { // If this feature is switched off in configuration the UI will be amended to not make the request to reset password available. diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 9da8e592a2..4c65e3dc20 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -67,14 +67,12 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IUserService _userService; private readonly ILocalizedTextService _localizedTextService; private readonly UmbracoMapper _umbracoMapper; - private readonly IEntityService _entityService; - private readonly IMediaService _mediaService; - private readonly IContentService _contentService; private readonly GlobalSettings _globalSettings; private readonly IBackOfficeUserManager _userManager; private readonly ILoggerFactory _loggerFactory; private readonly LinkGenerator _linkGenerator; private readonly IBackOfficeExternalLoginProviders _externalLogins; + private readonly UserEditorAuthorizationHelper _userEditorAuthorizationHelper; private readonly ILogger _logger; public UsersController( @@ -92,14 +90,12 @@ namespace Umbraco.Web.BackOffice.Controllers IUserService userService, ILocalizedTextService localizedTextService, UmbracoMapper umbracoMapper, - IEntityService entityService, - IMediaService mediaService, - IContentService contentService, IOptions globalSettings, IBackOfficeUserManager backOfficeUserManager, ILoggerFactory loggerFactory, LinkGenerator linkGenerator, - IBackOfficeExternalLoginProviders externalLogins) + IBackOfficeExternalLoginProviders externalLogins, + UserEditorAuthorizationHelper userEditorAuthorizationHelper) { _mediaFileSystem = mediaFileSystem; _contentSettings = contentSettings.Value; @@ -115,14 +111,12 @@ namespace Umbraco.Web.BackOffice.Controllers _userService = userService; _localizedTextService = localizedTextService; _umbracoMapper = umbracoMapper; - _entityService = entityService; - _mediaService = mediaService; - _contentService = contentService; _globalSettings = globalSettings.Value; _userManager = backOfficeUserManager; _loggerFactory = loggerFactory; _linkGenerator = linkGenerator; _externalLogins = externalLogins; + _userEditorAuthorizationHelper = userEditorAuthorizationHelper; _logger = _loggerFactory.CreateLogger(); } @@ -140,7 +134,7 @@ namespace Umbraco.Web.BackOffice.Controllers } [AppendUserModifiedHeader("id")] - [AdminUsersAuthorize] + [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] public IActionResult PostSetAvatar(int id, IList files) { return PostSetAvatarInternal(files, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, id); @@ -193,7 +187,7 @@ namespace Umbraco.Web.BackOffice.Controllers } [AppendUserModifiedHeader("id")] - [AdminUsersAuthorize] + [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] public ActionResult PostClearAvatar(int id) { var found = _userService.GetUserById(id); @@ -232,7 +226,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [AdminUsersAuthorize] + [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] public UserDisplay GetById(int id) { var user = _userService.GetUserById(id); @@ -250,7 +244,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [AdminUsersAuthorize] + [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] public IEnumerable GetByIds([FromJsonPath]int[] ids) { if (ids == null) @@ -367,8 +361,7 @@ namespace Umbraco.Web.BackOffice.Controllers CheckUniqueEmail(userSave.Email, null); //Perform authorization here to see if the current user can actually save this user with the info being requested - var authHelper = new UserEditorAuthorizationHelper(_contentService,_mediaService, _userService, _entityService); - var canSaveUser = authHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, null, null, null, userSave.UserGroups); + var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, null, null, null, userSave.UserGroups); if (canSaveUser == false) { throw new HttpResponseException(HttpStatusCode.Unauthorized, canSaveUser.Result); @@ -451,8 +444,7 @@ namespace Umbraco.Web.BackOffice.Controllers } //Perform authorization here to see if the current user can actually save this user with the info being requested - var authHelper = new UserEditorAuthorizationHelper(_contentService,_mediaService, _userService, _entityService); - var canSaveUser = authHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups); + var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups); if (canSaveUser == false) { return new ValidationErrorResult(canSaveUser.Result, StatusCodes.Status401Unauthorized); @@ -605,8 +597,7 @@ namespace Umbraco.Web.BackOffice.Controllers throw new HttpResponseException(HttpStatusCode.NotFound); //Perform authorization here to see if the current user can actually save this user with the info being requested - var authHelper = new UserEditorAuthorizationHelper(_contentService,_mediaService, _userService, _entityService); - var canSaveUser = authHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, userSave.StartContentIds, userSave.StartMediaIds, userSave.UserGroups); + var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, userSave.StartContentIds, userSave.StartMediaIds, userSave.UserGroups); if (canSaveUser == false) { throw new HttpResponseException(HttpStatusCode.Unauthorized, canSaveUser.Result); @@ -719,7 +710,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Disables the users with the given user ids /// /// - [AdminUsersAuthorize("userIds")] + [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] public IActionResult PostDisableUsers([FromQuery]int[] userIds) { var tryGetCurrentUserId = _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId(); @@ -750,7 +741,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Enables the users with the given user ids /// /// - [AdminUsersAuthorize("userIds")] + [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] public IActionResult PostEnableUsers([FromQuery]int[] userIds) { var users = _userService.GetUsersById(userIds).ToArray(); @@ -774,7 +765,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Unlocks the users with the given user ids /// /// - [AdminUsersAuthorize("userIds")] + [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] public async Task PostUnlockUsers([FromQuery]int[] userIds) { if (userIds.Length <= 0) return Ok(); @@ -807,7 +798,7 @@ namespace Umbraco.Web.BackOffice.Controllers _localizedTextService.Localize("speechBubbles/unlockUsersSuccess", new[] {(userIds.Length - notFound.Count).ToString()})); } - [AdminUsersAuthorize("userIds")] + [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] public IActionResult PostSetUserGroupsOnUsers([FromQuery]string[] userGroupAliases, [FromQuery]int[] userIds) { var users = _userService.GetUsersById(userIds).ToArray(); @@ -833,7 +824,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Limited to users that haven't logged in to avoid issues with related records constrained /// with a foreign key on the user Id /// - [AdminUsersAuthorize] + [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] public IActionResult PostDeleteNonLoggedInUser(int id) { var user = _userService.GetUserById(id); diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 00ab69f50e..7f71847c6f 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -115,11 +115,27 @@ namespace Umbraco.Extensions /// private static void AddBackOfficeAuthorizationPolicies(this IServiceCollection services) { + // NOTE: Even though we are registering these handlers globally they will only actually execute their logic for + // any auth defining a matching requirement. We don't want to get in the way of end-users own aspnet logic + // and although these will trigger for any of their requests that need authorization, the logic won't actually execute. + // Basically all registered IAuthorizationHandler will execute for all requests requiring authorization but their logic + // won't trigger unless the requirement/policy matches. + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddAuthorization(options => { + options.AddPolicy(AuthorizationPolicies.AdminUserEditsRequireAdmin, policy => + { + policy.Requirements.Add(new AdminUsersAuthorizeRequirement()); + policy.Requirements.Add(new AdminUsersAuthorizeRequirement("userIds")); + }); + + options.AddPolicy(AuthorizationPolicies.DenyLocalLoginIfConfigured, policy => + policy.Requirements.Add(new DenyLocalLoginRequirement())); options.AddPolicy(AuthorizationPolicies.SectionAccessContent, policy => policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Content))); diff --git a/src/Umbraco.Web.BackOffice/Filters/AdminUsersAuthorizeAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/AdminUsersAuthorizeAttribute.cs deleted file mode 100644 index 867b2c0a24..0000000000 --- a/src/Umbraco.Web.BackOffice/Filters/AdminUsersAuthorizeAttribute.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Linq; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Umbraco.Core; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Web.Editors; -using Umbraco.Web.Security; - -namespace Umbraco.Web.BackOffice.Filters -{ - /// - /// if the users being edited is an admin then we must ensure that the current user is also an admin - /// - /// - /// This will authorize against one or multiple ids - /// - public sealed class AdminUsersAuthorizeAttribute : TypeFilterAttribute - { - - public AdminUsersAuthorizeAttribute(string parameterName): base(typeof(AdminUsersAuthorizeFilter)) - { - Arguments = new object[] { parameterName }; - } - - public AdminUsersAuthorizeAttribute() : this("id") - { - } - - private class AdminUsersAuthorizeFilter : IAuthorizationFilter - { - private readonly string _parameterName; - private readonly IRequestAccessor _requestAccessor; - private readonly IUserService _userService; - private readonly IContentService _contentService; - private readonly IMediaService _mediaService; - private readonly IEntityService _entityService; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - - public AdminUsersAuthorizeFilter( - IRequestAccessor requestAccessor, - IUserService userService, - IContentService contentService, - IMediaService mediaService, - IEntityService entityService, - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - string parameterName) - { - _requestAccessor = requestAccessor; - _userService = userService; - _contentService = contentService; - _mediaService = mediaService; - _entityService = entityService; - _backofficeSecurityAccessor = backofficeSecurityAccessor; - _parameterName = parameterName; - } - - public void OnAuthorization(AuthorizationFilterContext context) - { - if (!IsAuthorized(context)) - { - context.Result = new ForbidResult(); - } - } - - private bool IsAuthorized(AuthorizationFilterContext actionContext) - { - int[] userIds; - - if (int.TryParse(_requestAccessor.GetRequestValue(_parameterName), out var userId)) - { - var intUserId = userId.TryConvertTo(); - if (intUserId) - userIds = new[] { intUserId.Result }; - else return true; - } - else - { - var queryString = actionContext.HttpContext.Request.Query; - var ids = queryString.Where(x => x.Key == _parameterName).ToArray(); - if (ids.Length == 0) - return true; - userIds = ids.Select(x => x.Value.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); - } - - if (userIds.Length == 0) return true; - - var users = _userService.GetUsersById(userIds); - var authHelper = new UserEditorAuthorizationHelper(_contentService, _mediaService, _userService, _entityService); - return users.All(user => authHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, null) != false); - } - } - - - } -} diff --git a/src/Umbraco.Web.BackOffice/Filters/DenyLocalLoginAuthorizationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/DenyLocalLoginAuthorizationAttribute.cs deleted file mode 100644 index a5d22d702d..0000000000 --- a/src/Umbraco.Web.BackOffice/Filters/DenyLocalLoginAuthorizationAttribute.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using System; -using Umbraco.Web.Common.Security; - -namespace Umbraco.Web.Editors.Filters -{ - public sealed class DenyLocalLoginAuthorizationAttribute : TypeFilterAttribute - { - public DenyLocalLoginAuthorizationAttribute() : base(typeof(DenyLocalLoginFilter)) - { - } - - private class DenyLocalLoginFilter : IAuthorizationFilter - { - private readonly IBackOfficeExternalLoginProviders _externalLogins; - - public DenyLocalLoginFilter(IBackOfficeExternalLoginProviders externalLogins) - { - _externalLogins = externalLogins; - } - - public void OnAuthorization(AuthorizationFilterContext context) - { - if (_externalLogins.HasDenyLocalLogin()) - { - // if there is a deny local login provider then we cannot authorize - context.Result = new ForbidResult(); - } - } - } - - } -} From 9c5ce631a92a92ca6005f410fd00f1abc61ca67e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Nov 2020 12:12:54 +1100 Subject: [PATCH 05/25] removes UmbracoWebApiRequireHttpsAttribute since we dont' need it --- .../UmbracoAuthorizedApiController.cs | 2 +- .../UmbracoWebApiRequireHttpsAttribute.cs | 74 ------------------- 2 files changed, 1 insertion(+), 75 deletions(-) delete mode 100644 src/Umbraco.Web.BackOffice/Filters/UmbracoWebApiRequireHttpsAttribute.cs diff --git a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs index e3d779d61d..080671d3dd 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs @@ -18,7 +18,7 @@ namespace Umbraco.Web.BackOffice.Controllers [UmbracoUserTimeoutFilter] [UmbracoBackOfficeAuthorize] [DisableBrowserCache] - [UmbracoWebApiRequireHttps] + [RequireHttps] [CheckIfUserTicketDataIsStale] [MiddlewareFilter(typeof(UnhandledExceptionLoggerFilter))] public abstract class UmbracoAuthorizedApiController : UmbracoApiController diff --git a/src/Umbraco.Web.BackOffice/Filters/UmbracoWebApiRequireHttpsAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/UmbracoWebApiRequireHttpsAttribute.cs deleted file mode 100644 index e2a1d942d9..0000000000 --- a/src/Umbraco.Web.BackOffice/Filters/UmbracoWebApiRequireHttpsAttribute.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Options; -using Umbraco.Core.Configuration.Models; - -namespace Umbraco.Web.BackOffice.Filters -{ - /// - /// If Umbraco.Core.UseHttps property in web.config is set to true, this filter will redirect any http access to https. - /// - /// - /// This will only redirect Head/Get requests, otherwise will respond with text - /// - /// References: - /// http://issues.umbraco.org/issue/U4-8542 - /// https://blogs.msdn.microsoft.com/carlosfigueira/2012/03/09/implementing-requirehttps-with-asp-net-web-api/ - /// - public class UmbracoWebApiRequireHttpsAttribute : TypeFilterAttribute - { - public UmbracoWebApiRequireHttpsAttribute() : base(typeof(UmbracoWebApiRequireHttpsFilter)) - { - Arguments = Array.Empty(); - } - } - - public class UmbracoWebApiRequireHttpsFilter: IAuthorizationFilter - { - private readonly GlobalSettings _globalSettings; - - public UmbracoWebApiRequireHttpsFilter(IOptions globalSettings) - { - _globalSettings = globalSettings.Value; - } - - public void OnAuthorization(AuthorizationFilterContext context) - { - var request = context.HttpContext.Request; - if (_globalSettings.UseHttps && request.Scheme != Uri.UriSchemeHttps) - { - var uri = new UriBuilder() - { - Scheme = Uri.UriSchemeHttps, - Host = request.Host.Value, - Path = request.Path, - Query = request.QueryString.ToUriComponent(), - Port = 443 - }; - var body = string.Format("

The resource can be found at {0}.

", - uri.Uri.AbsoluteUri); - if (request.Method.Equals(HttpMethod.Get.ToString()) || request.Method.Equals(HttpMethod.Head.ToString())) - { - context.HttpContext.Response.Headers.Add("Location", uri.Uri.ToString()); - context.Result = new ObjectResult(body) - { - StatusCode = (int)HttpStatusCode.Found, - }; - - } - else - { - context.Result = new ObjectResult(body) - { - StatusCode = (int)HttpStatusCode.NotFound - }; - } - - - } - } - } -} From f32399292e242de1661cd0bac5e5f1cdc06736d5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Nov 2020 12:23:09 +1100 Subject: [PATCH 06/25] Migrates UserGroupAuthorizationAttribute to authz policies --- .../AdminUsersAuthorizeHandler.cs | 4 +- .../AdminUsersAuthorizeRequirement.cs | 1 + .../Authorization/AuthorizationPolicies.cs | 1 + .../UserGroupAuthorizationHandler.cs | 76 ++++++++++++++++ .../UserGroupAuthorizeRequirement.cs | 17 ++++ .../Controllers/UserGroupsController.cs | 4 +- .../BackOfficeServiceCollectionExtensions.cs | 7 ++ .../UserGroupAuthorizationAttribute.cs | 87 ------------------- 8 files changed, 106 insertions(+), 91 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizationHandler.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizeRequirement.cs delete mode 100644 src/Umbraco.Web.BackOffice/Filters/UserGroupAuthorizationAttribute.cs diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs index 30f9502dde..9070246c56 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.BackOffice.Authorization protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminUsersAuthorizeRequirement requirement) { - var isAuth = IsAuthorized(context, requirement); + var isAuth = IsAuthorized(requirement); if (!isAuth.HasValue || isAuth.Value) { context.Succeed(requirement); @@ -45,7 +45,7 @@ namespace Umbraco.Web.BackOffice.Authorization return Task.CompletedTask; } - private bool? IsAuthorized(AuthorizationHandlerContext context, AdminUsersAuthorizeRequirement requirement) + private bool? IsAuthorized(AdminUsersAuthorizeRequirement requirement) { int[] userIds; diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeRequirement.cs index 5b150db080..a4fec9c729 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeRequirement.cs @@ -2,6 +2,7 @@ namespace Umbraco.Web.BackOffice.Authorization { + /// /// Authorization requirement for the /// diff --git a/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs index bcf68688d5..2c8b4c7000 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs @@ -5,6 +5,7 @@ /// public static class AuthorizationPolicies { + public const string UserBelongsToUserGroupInRequest = nameof(UserBelongsToUserGroupInRequest); public const string AdminUserEditsRequireAdmin = nameof(AdminUserEditsRequireAdmin); public const string DenyLocalLoginIfConfigured = nameof(DenyLocalLoginIfConfigured); diff --git a/src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizationHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizationHandler.cs new file mode 100644 index 0000000000..f34c58411f --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizationHandler.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using System.Linq; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Security; +using Umbraco.Core.Services; +using Umbraco.Web.BackOffice.Controllers; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Authorizes that the current user has access to the user group Id in the request + /// + public class UserGroupAuthorizationHandler : AuthorizationHandler + { + private readonly IHttpContextAccessor _httpContextAcessor; + private readonly IUserService _userService; + private readonly IContentService _contentService; + private readonly IMediaService _mediaService; + private readonly IEntityService _entityService; + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + + public UserGroupAuthorizationHandler(IHttpContextAccessor httpContextAcessor, + IUserService userService, + IContentService contentService, + IMediaService mediaService, + IEntityService entityService, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + { + _httpContextAcessor = httpContextAcessor; + _userService = userService; + _contentService = contentService; + _mediaService = mediaService; + _entityService = entityService; + _backofficeSecurityAccessor = backofficeSecurityAccessor; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserGroupAuthorizeRequirement requirement) + { + var isAuth = IsAuthorized(requirement); + if (!isAuth.HasValue || isAuth.Value) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + + private bool? IsAuthorized(UserGroupAuthorizeRequirement requirement) + { + var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + + var queryString = _httpContextAcessor.HttpContext?.Request.Query; + if (queryString == null) + return null; + + var ids = queryString.Where(x => x.Key == requirement.QueryStringName).ToArray(); + if (ids.Length == 0) + return null; + + var intIds = ids.Select(x => x.Value.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); + var authHelper = new UserGroupEditorAuthorizationHelper( + _userService, + _contentService, + _mediaService, + _entityService); + return authHelper.AuthorizeGroupAccess(currentUser, intIds); + + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizeRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizeRequirement.cs new file mode 100644 index 0000000000..5a411b89fd --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizeRequirement.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Authorization requirement for the + /// + public class UserGroupAuthorizeRequirement : IAuthorizationRequirement + { + public UserGroupAuthorizeRequirement(string queryStringName = "id") + { + QueryStringName = queryStringName; + } + + public string QueryStringName { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs index 940f2a76df..f9048749bd 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs @@ -166,7 +166,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Return a user group /// /// - [UserGroupAuthorization("id")] + [Authorize(Policy = AuthorizationPolicies.UserBelongsToUserGroupInRequest)] public ActionResult GetUserGroup(int id) { var found = _userService.GetUserGroupById(id); @@ -180,7 +180,7 @@ namespace Umbraco.Web.BackOffice.Controllers [HttpPost] [HttpDelete] - [UserGroupAuthorization("userGroupIds")] + [Authorize(Policy = AuthorizationPolicies.UserBelongsToUserGroupInRequest)] public IActionResult PostDeleteUserGroups([FromQuery] int[] userGroupIds) { var userGroups = _userService.GetAllUserGroups(userGroupIds) diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 7f71847c6f..46efb8e678 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -124,6 +124,7 @@ namespace Umbraco.Extensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddAuthorization(options => @@ -134,6 +135,12 @@ namespace Umbraco.Extensions policy.Requirements.Add(new AdminUsersAuthorizeRequirement("userIds")); }); + options.AddPolicy(AuthorizationPolicies.UserBelongsToUserGroupInRequest, policy => + { + policy.Requirements.Add(new UserGroupAuthorizeRequirement()); + policy.Requirements.Add(new UserGroupAuthorizeRequirement("userGroupIds")); + }); + options.AddPolicy(AuthorizationPolicies.DenyLocalLoginIfConfigured, policy => policy.Requirements.Add(new DenyLocalLoginRequirement())); diff --git a/src/Umbraco.Web.BackOffice/Filters/UserGroupAuthorizationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/UserGroupAuthorizationAttribute.cs deleted file mode 100644 index 5fa9dd54be..0000000000 --- a/src/Umbraco.Web.BackOffice/Filters/UserGroupAuthorizationAttribute.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Linq; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Umbraco.Core; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Controllers; -using Umbraco.Web.Security; - -namespace Umbraco.Web.BackOffice.Filters -{ - /// - /// Authorizes that the current user has access to the user group Id in the request - /// - /// - /// This will authorize against one or multiple ids - /// - public sealed class UserGroupAuthorizationAttribute : TypeFilterAttribute - { - - public UserGroupAuthorizationAttribute(string parameterName): base(typeof(UserGroupAuthorizationFilter)) - { - Arguments = new object[] { parameterName }; - } - - public UserGroupAuthorizationAttribute() : this("id") - { - } - - private class UserGroupAuthorizationFilter : IAuthorizationFilter - { - private readonly string _parameterName; - private readonly IRequestAccessor _requestAccessor; - private readonly IUserService _userService; - private readonly IContentService _contentService; - private readonly IMediaService _mediaService; - private readonly IEntityService _entityService; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - - public UserGroupAuthorizationFilter( - IRequestAccessor requestAccessor, - IUserService userService, - IContentService contentService, - IMediaService mediaService, - IEntityService entityService, - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - string parameterName) - { - _requestAccessor = requestAccessor; - _userService = userService; - _contentService = contentService; - _mediaService = mediaService; - _entityService = entityService; - _backofficeSecurityAccessor = backofficeSecurityAccessor; - _parameterName = parameterName; - } - - public void OnAuthorization(AuthorizationFilterContext context) - { - if (!IsAuthorized(context)) - { - context.Result = new ForbidResult(); - } - } - - private bool IsAuthorized(AuthorizationFilterContext actionContext) - { - var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; - - var queryString = actionContext.HttpContext.Request.Query; - - var ids = queryString.Where(x => x.Key == _parameterName).ToArray(); - if (ids.Length == 0) - return true; - - var intIds = ids.Select(x => x.Value.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); - var authHelper = new UserGroupEditorAuthorizationHelper( - _userService, - _contentService, - _mediaService, - _entityService); - return authHelper.AuthorizeGroupAccess(currentUser, intIds); - - } - } - } -} From e7b4c5a8117a9a800e4d34621e25ecfa13f7db4d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Nov 2020 12:40:29 +1100 Subject: [PATCH 07/25] Converts FeatureAuthorize to authz policy --- .../ModelsBuilderDashboardController.cs | 2 +- .../Controllers/AuthenticationController.cs | 2 +- .../Controllers/CodeFileController.cs | 2 +- .../Controllers/ContentController.cs | 2 +- .../Controllers/ContentTypeController.cs | 2 +- .../Controllers/DashboardController.cs | 1 - .../Controllers/DataTypeController.cs | 2 +- .../Controllers/DictionaryController.cs | 2 +- .../Controllers/LanguageController.cs | 2 +- .../Controllers/LogController.cs | 2 +- .../Controllers/MacrosController.cs | 2 +- .../Controllers/MediaController.cs | 2 +- .../Controllers/MediaTypeController.cs | 2 +- .../Controllers/MemberController.cs | 2 +- .../Controllers/MemberGroupController.cs | 2 +- .../Controllers/MemberTypeController.cs | 2 +- .../Controllers/PackageController.cs | 2 +- .../Controllers/PackageInstallController.cs | 2 +- .../Controllers/RelationController.cs | 2 +- .../Controllers/RelationTypeController.cs | 2 +- .../Controllers/TemplateController.cs | 2 +- .../Controllers/TinyMceController.cs | 3 +- .../Controllers/UserGroupsController.cs | 2 +- .../Controllers/UsersController.cs | 2 +- .../BackOfficeServiceCollectionExtensions.cs | 2 + .../FilterAllowedOutgoingContentAttribute.cs | 2 - .../HealthCheck/HealthCheckController.cs | 2 +- .../Profiling/WebProfilingController.cs | 2 +- .../Trees/ContentBlueprintTreeController.cs | 2 +- .../Trees/ContentTreeController.cs | 2 +- .../Trees/ContentTypeTreeController.cs | 2 +- .../Trees/DataTypeTreeController.cs | 2 +- .../Trees/DictionaryTreeController.cs | 2 +- .../Trees/LanguageTreeController.cs | 2 +- .../Trees/LogViewerTreeController.cs | 2 +- .../Trees/MacrosTreeController.cs | 2 +- .../Trees/MediaTreeController.cs | 2 +- .../Trees/MediaTypeTreeController.cs | 2 +- .../Trees/MemberGroupTreeController.cs | 2 +- .../Trees/MemberTreeController.cs | 2 +- .../Trees/MemberTypeTreeController.cs | 2 +- .../Trees/PackagesTreeController.cs | 2 +- .../Trees/PartialViewMacrosTreeController.cs | 2 +- .../Trees/PartialViewsTreeController.cs | 2 +- .../Trees/RelationTypeTreeController.cs | 2 +- .../Trees/TemplatesTreeController.cs | 2 +- .../Trees/UserTreeController.cs | 2 +- .../Authorization/AuthorizationPolicies.cs | 6 ++- .../Authorization/FeatureAuthorizeHandler.cs | 47 +++++++++++++++++ .../FeatureAuthorizeRequirement.cs | 12 +++++ .../Controllers/UmbracoApiControllerBase.cs | 7 +-- .../ApplicationBuilderExtensions.cs | 3 -- .../Extensions/ServiceCollectionExtensions.cs | 22 ++++++++ .../Filters/FeatureAuthorizeAttribute.cs | 51 ------------------- 54 files changed, 135 insertions(+), 107 deletions(-) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/Authorization/AuthorizationPolicies.cs (95%) create mode 100644 src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs create mode 100644 src/Umbraco.Web.Common/Authorization/FeatureAuthorizeRequirement.cs create mode 100644 src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs delete mode 100644 src/Umbraco.Web.Common/Filters/FeatureAuthorizeAttribute.cs diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs index 76c33e8de4..0b67498f01 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs @@ -8,9 +8,9 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Exceptions; using Umbraco.Core.Hosting; using Umbraco.ModelsBuilder.Embedded.Building; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.Authorization; namespace Umbraco.ModelsBuilder.Embedded.BackOffice { diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 98c36044ca..495adc3c4a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -31,7 +31,7 @@ using Umbraco.Web.Security; using Constants = Umbraco.Core.Constants; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index 118dbf0926..a5e6051d45 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -15,10 +15,10 @@ using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Core.Strings.Css; using Umbraco.Extensions; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Models.ContentEditing; using Stylesheet = Umbraco.Core.Models.Stylesheet; diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index f91bb9c124..c22db868b1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -38,7 +38,7 @@ using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Common.Filters; using Umbraco.Web.Models.Mapping; using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index 33f19141c3..4088e469c0 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -37,7 +37,7 @@ using Umbraco.Core.Configuration.Models; using Microsoft.Extensions.Options; using Umbraco.Core.Serialization; using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs index 4d12f8db0c..3f797de9c8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -20,7 +20,6 @@ using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Filters; -using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs index eb2108a5e2..664eab13cc 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs @@ -15,9 +15,9 @@ using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Serialization; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Editors; using Umbraco.Web.Models.ContentEditing; diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index 00e10852c7..c7f86e12a1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -19,7 +19,7 @@ using Constants = Umbraco.Core.Constants; using Umbraco.Core.Configuration.Models; using Microsoft.Extensions.Options; using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index a3e6c83ae6..21b205de0f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -11,9 +11,9 @@ using Umbraco.Core.Configuration.Models; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Editors; using Language = Umbraco.Web.Models.ContentEditing.Language; diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs index a8210eb20b..acdd9721e4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs @@ -11,9 +11,9 @@ using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Security; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Security; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs index 8b9fb8e7d2..3ca89fa5ff 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs @@ -19,8 +19,8 @@ using Umbraco.Web.Security; using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Security; -using Umbraco.Web.BackOffice.Authorization; using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 5d8b9c0e97..4c68b7dfa6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -40,8 +40,8 @@ using Umbraco.Web.Common.Exceptions; using Umbraco.Web.ContentApps; using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; -using Umbraco.Web.BackOffice.Authorization; using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs index 84a3e5a260..334b1adbe8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs @@ -11,9 +11,9 @@ using Umbraco.Core.Models; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Strings; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Editors; using Umbraco.Web.Models.ContentEditing; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 0b090a5e47..a97ed9c2ad 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -25,10 +25,10 @@ using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Extensions; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.ModelBinders; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Common.Filters; using Umbraco.Web.ContentApps; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs index 12f26f484b..a7cbaf96c1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs @@ -8,9 +8,9 @@ using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs index dccc2b7907..e203386958 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs @@ -25,7 +25,7 @@ using Umbraco.Web.Editors; using Umbraco.Web.Routing; using Umbraco.Web.Security; using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index de855ab421..36bf4d2fca 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs @@ -13,9 +13,9 @@ using Umbraco.Core.Hosting; using Umbraco.Core.Models.Packaging; using Umbraco.Core.Security; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Security; diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs index e08dfeff60..961ec388f7 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs @@ -23,7 +23,7 @@ using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Security; using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs index e9812aa215..5646c7f1aa 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs @@ -9,10 +9,10 @@ using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Editors; using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs index 7c80ebcc81..b2706babee 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs @@ -15,8 +15,8 @@ using Umbraco.Core.Mapping; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; -using Umbraco.Web.BackOffice.Authorization; using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs b/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs index d60e87a23b..fe75cf5a0a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs @@ -10,9 +10,9 @@ using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Strings; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; diff --git a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs index 8f7b5c5a0e..edaaa4f1e3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs @@ -15,11 +15,10 @@ using Umbraco.Core.Hosting; using Umbraco.Core.IO; using Umbraco.Core.Media; using Umbraco.Core.Strings; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; - +using Umbraco.Web.Common.Authorization; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Controllers diff --git a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs index f9048749bd..64aef74257 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs @@ -16,8 +16,8 @@ using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Security; using Constants = Umbraco.Core.Constants; -using Umbraco.Web.BackOffice.Authorization; using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 4c65e3dc20..809afff364 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -42,8 +42,8 @@ using Task = System.Threading.Tasks.Task; using Umbraco.Net; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Security; -using Umbraco.Web.BackOffice.Authorization; using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 46efb8e678..482352cc74 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -14,6 +14,7 @@ using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.AspNetCore; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Security; namespace Umbraco.Extensions @@ -45,6 +46,7 @@ namespace Umbraco.Extensions services.ConfigureOptions(); + services.AddUmbracoCommonAuthorizationPolicies(); services.AddBackOfficeAuthorizationPolicies(); } diff --git a/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs index e6735d01e8..38c0333d8b 100644 --- a/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs @@ -10,8 +10,6 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Actions; -using Umbraco.Web.Security; -using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.BackOffice.Filters { diff --git a/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs b/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs index 44d8161519..002cfe4d7b 100644 --- a/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs +++ b/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs @@ -10,8 +10,8 @@ using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Microsoft.Extensions.Logging; -using Umbraco.Web.BackOffice.Authorization; using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.HealthCheck { diff --git a/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs b/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs index ae936984b0..0908522d9e 100644 --- a/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs +++ b/src/Umbraco.Web.BackOffice/Profiling/WebProfilingController.cs @@ -5,7 +5,7 @@ using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Controllers; using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Profiling { diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs index eba16f288f..e232bf03b9 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs @@ -7,9 +7,9 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Services; using Umbraco.Web.Actions; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.Trees; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs index 6bc19e058c..16dd446d49 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs @@ -21,7 +21,7 @@ using Umbraco.Core.Configuration.Models; using Microsoft.Extensions.Options; using Umbraco.Web.Trees; using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Trees { diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs index 15c206e65f..8b5286bdd2 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs @@ -7,9 +7,9 @@ using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Actions; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Trees; using Umbraco.Web.Search; diff --git a/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs index f4e42729f0..ab2bfdb8d4 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs @@ -14,8 +14,8 @@ using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; -using Umbraco.Web.BackOffice.Authorization; using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Trees { diff --git a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs index 163e1607a7..ffa9e00b0e 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs @@ -6,9 +6,9 @@ using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Actions; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.Trees; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; diff --git a/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs index 9ac3d0d43c..ecd1c954ac 100644 --- a/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.Trees; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; diff --git a/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs index 4e484c447c..b03b2d9926 100644 --- a/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.Trees; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; diff --git a/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs index 1df741ce81..518c1b5495 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs @@ -8,8 +8,8 @@ using Umbraco.Web.Common.Attributes; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Constants = Umbraco.Core.Constants; -using Umbraco.Web.BackOffice.Authorization; using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Trees { diff --git a/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs index 2ae60047a3..ece4013d0b 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs @@ -21,7 +21,7 @@ using Umbraco.Web.Security; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Trees { diff --git a/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs index de8e51719d..cd64e23067 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs @@ -13,8 +13,8 @@ using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; -using Umbraco.Web.BackOffice.Authorization; using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Trees { diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs index 440a3439f2..817b32f301 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs @@ -4,9 +4,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.Trees; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs index e1b898c142..4ebd8f7cc5 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs @@ -20,8 +20,8 @@ using Constants = Umbraco.Core.Constants; using Umbraco.Web.Security; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; -using Umbraco.Web.BackOffice.Authorization; using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Trees { diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs index 9f560700e8..be400bef39 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs @@ -5,10 +5,10 @@ using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Trees; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Trees; using Umbraco.Web.Search; diff --git a/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs index 08051c6ab3..5c96bb4d64 100644 --- a/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.Trees; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; diff --git a/src/Umbraco.Web.BackOffice/Trees/PartialViewMacrosTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/PartialViewMacrosTreeController.cs index c5f95ebc5e..484ea21b2f 100644 --- a/src/Umbraco.Web.BackOffice/Trees/PartialViewMacrosTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/PartialViewMacrosTreeController.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Umbraco.Core.IO; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Constants = Umbraco.Core.Constants; diff --git a/src/Umbraco.Web.BackOffice/Trees/PartialViewsTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/PartialViewsTreeController.cs index 8fd7d8de6a..b648bd797f 100644 --- a/src/Umbraco.Web.BackOffice/Trees/PartialViewsTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/PartialViewsTreeController.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Authorization; using Umbraco.Core.IO; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Composing; using Umbraco.Web.Mvc; using Umbraco.Web.Trees; diff --git a/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs index ceb8b9a032..a36c2f36a9 100644 --- a/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs @@ -9,8 +9,8 @@ using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; -using Umbraco.Web.BackOffice.Authorization; using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Trees { diff --git a/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs index 2054a6b5f4..361875a41b 100644 --- a/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs @@ -9,9 +9,9 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Services; using Umbraco.Extensions; using Umbraco.Web.Actions; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Trees; using Umbraco.Web.Search; diff --git a/src/Umbraco.Web.BackOffice/Trees/UserTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/UserTreeController.cs index 33e943e9b4..960ed76ac5 100644 --- a/src/Umbraco.Web.BackOffice/Trees/UserTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/UserTreeController.cs @@ -1,8 +1,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.Trees; using Umbraco.Web.Trees; using Umbraco.Web.WebApi; diff --git a/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs similarity index 95% rename from src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs rename to src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index 2c8b4c7000..e4add40df9 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -1,10 +1,12 @@ -namespace Umbraco.Web.BackOffice.Authorization +namespace Umbraco.Web.Common.Authorization { /// /// A list of authorization policy names for use in the back office /// public static class AuthorizationPolicies { + public const string UmbracoFeatureEnabled = nameof(UmbracoFeatureEnabled); + public const string UserBelongsToUserGroupInRequest = nameof(UserBelongsToUserGroupInRequest); public const string AdminUserEditsRequireAdmin = nameof(AdminUserEditsRequireAdmin); public const string DenyLocalLoginIfConfigured = nameof(DenyLocalLoginIfConfigured); @@ -25,7 +27,7 @@ public const string SectionAccessForMemberTree = nameof(SectionAccessForMemberTree); public const string SectionAccessForMediaTree = nameof(SectionAccessForMediaTree); public const string SectionAccessForContentTree = nameof(SectionAccessForContentTree); - public const string SectionAccessForDataTypeReading = nameof(SectionAccessForDataTypeReading); + public const string SectionAccessForDataTypeReading = nameof(SectionAccessForDataTypeReading); // Single tree access diff --git a/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs new file mode 100644 index 0000000000..d8b9ab7ff4 --- /dev/null +++ b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; +using System.Threading.Tasks; +using Umbraco.Web.Features; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Ensures that the controller is an authorized feature. + /// + public class FeatureAuthorizeHandler : AuthorizationHandler + { + private readonly UmbracoFeatures _umbracoFeatures; + + public FeatureAuthorizeHandler(UmbracoFeatures umbracoFeatures) + { + _umbracoFeatures = umbracoFeatures; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FeatureAuthorizeRequirement requirement) + { + var allowed = IsAllowed(context); + if (!allowed.HasValue || allowed.Value) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + return Task.CompletedTask; + } + + private bool? IsAllowed(AuthorizationHandlerContext context) + { + if (context.Resource is Endpoint endpoint) + { + var actionDescriptor = endpoint.Metadata.GetMetadata(); + var controllerType = actionDescriptor.ControllerTypeInfo.AsType(); + return _umbracoFeatures.IsControllerEnabled(controllerType); + } + + return null; + } + } +} diff --git a/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeRequirement.cs b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeRequirement.cs new file mode 100644 index 0000000000..87614d7f19 --- /dev/null +++ b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeRequirement.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + + /// + /// Authorization requirement for the + /// + public class FeatureAuthorizeRequirement : IAuthorizationRequirement + { + } +} diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs index 787da05ca4..87b7ee7c72 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs @@ -1,8 +1,9 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Filters; using Umbraco.Web.Features; -using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Common.Controllers { @@ -13,7 +14,7 @@ namespace Umbraco.Web.Common.Controllers /// These controllers are NOT auto-routed. /// The base class is which are netcore API controllers without any view support /// - [FeatureAuthorize] // TODO: This could be part of our conventions + [Authorize(Policy = AuthorizationPolicies.UmbracoFeatureEnabled)] // TODO: This could be part of our conventions [TypeFilter(typeof(HttpResponseExceptionFilter))] // TODO: This could be part of our conventions [UmbracoApiController] public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 36d4ddbd42..5c1d81b28b 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Net; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -8,9 +7,7 @@ using Smidge; using Smidge.Nuglify; using StackExchange.Profiling; using Umbraco.Core; -using Umbraco.Core.Composing; using Umbraco.Core.Hosting; -using Umbraco.Core.Runtime; using Umbraco.Infrastructure.Logging.Serilog.Enrichers; using Umbraco.Web.Common.Middleware; diff --git a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..0a51ace294 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; + +namespace Umbraco.Extensions +{ + public static class ServiceCollectionExtensions + { + public static void AddUmbracoCommonAuthorizationPolicies(this IServiceCollection services) + { + services.AddSingleton(); + + services.AddAuthorization(options => + { + options.AddPolicy(AuthorizationPolicies.UmbracoFeatureEnabled, policy => + policy.Requirements.Add(new FeatureAuthorizeRequirement())); + }); + } + } + +} diff --git a/src/Umbraco.Web.Common/Filters/FeatureAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Filters/FeatureAuthorizeAttribute.cs deleted file mode 100644 index 061225334a..0000000000 --- a/src/Umbraco.Web.Common/Filters/FeatureAuthorizeAttribute.cs +++ /dev/null @@ -1,51 +0,0 @@ - -using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Web.Features; -using Umbraco.Core; -using Umbraco.Web.Install; - -namespace Umbraco.Web.WebApi.Filters -{ - /// - /// Ensures that the controller is an authorized feature. - /// - /// Else returns unauthorized. - public class FeatureAuthorizeAttribute : TypeFilterAttribute - { - public FeatureAuthorizeAttribute() : base(typeof(FeatureAuthorizeFilter)) - { - } - - private class FeatureAuthorizeFilter : IAuthorizationFilter - { - public void OnAuthorization(AuthorizationFilterContext context) - { - var serviceProvider = context.HttpContext.RequestServices; - var umbracoFeatures = serviceProvider.GetService(); - - if (!IsAllowed(context, umbracoFeatures)) - { - context.Result = new ForbidResult(); - } - } - - private static bool IsAllowed(AuthorizationFilterContext context, UmbracoFeatures umbracoFeatures) - { - // if no features resolver has been set then return true, this will occur in unit - // tests and we don't want users to have to set a resolver - //just so their unit tests work. - - if (umbracoFeatures == null) return true; - if (!(context.ActionDescriptor is ControllerActionDescriptor contextActionDescriptor)) return true; - - var controllerType = contextActionDescriptor.ControllerTypeInfo.AsType(); - return umbracoFeatures.IsControllerEnabled(controllerType); - } - } - } -} From 84c2f75a27a58c220c75254c629e337a3183ef13 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Nov 2020 14:59:08 +1100 Subject: [PATCH 08/25] Adds notes, only run back office authz policies on the back office scheme --- .../Controllers/PreviewController.cs | 3 + .../BackOfficeServiceCollectionExtensions.cs | 187 ++++++++++++++---- .../UmbracoBackOfficeAuthorizeAttribute.cs | 107 ++++++++++ .../UmbracoBackOfficeAuthorizeFilter.cs | 110 ----------- 4 files changed, 257 insertions(+), 150 deletions(-) delete mode 100644 src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeFilter.cs diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index b8db09f9b7..d83123edc6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -64,6 +64,9 @@ namespace Umbraco.Web.BackOffice.Controllers _viewEngines = viewEngines; } + // TODO: This should really be refactored. Redirection/Challenge is part of Authentication, not part of authorization directly + // We only use this redirectToUmbracoLogin flag in this one instance. I think this + // should be handled as part of the preview authentication process instead. [UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: true, requireApproval : false)] [DisableBrowserCache] public ActionResult Index() diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 482352cc74..06e4f1ad4d 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -118,10 +118,7 @@ namespace Umbraco.Extensions private static void AddBackOfficeAuthorizationPolicies(this IServiceCollection services) { // NOTE: Even though we are registering these handlers globally they will only actually execute their logic for - // any auth defining a matching requirement. We don't want to get in the way of end-users own aspnet logic - // and although these will trigger for any of their requests that need authorization, the logic won't actually execute. - // Basically all registered IAuthorizationHandler will execute for all requests requiring authorization but their logic - // won't trigger unless the requirement/policy matches. + // any auth defining a matching requirement and scheme. services.AddSingleton(); services.AddSingleton(); @@ -133,135 +130,245 @@ namespace Umbraco.Extensions { options.AddPolicy(AuthorizationPolicies.AdminUserEditsRequireAdmin, policy => { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); policy.Requirements.Add(new AdminUsersAuthorizeRequirement()); policy.Requirements.Add(new AdminUsersAuthorizeRequirement("userIds")); }); options.AddPolicy(AuthorizationPolicies.UserBelongsToUserGroupInRequest, policy => { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); policy.Requirements.Add(new UserGroupAuthorizeRequirement()); policy.Requirements.Add(new UserGroupAuthorizeRequirement("userGroupIds")); }); options.AddPolicy(AuthorizationPolicies.DenyLocalLoginIfConfigured, policy => - policy.Requirements.Add(new DenyLocalLoginRequirement())); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new DenyLocalLoginRequirement()); + }); options.AddPolicy(AuthorizationPolicies.SectionAccessContent, policy => - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Content))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Content)); + }); options.AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, policy => - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Content, Constants.Applications.Media))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Content, Constants.Applications.Media)); + }); options.AddPolicy(AuthorizationPolicies.SectionAccessUsers, policy => - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Users))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Users)); + }); options.AddPolicy(AuthorizationPolicies.SectionAccessForTinyMce, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); policy.Requirements.Add(new SectionAliasesRequirement( - Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members))); + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); + }); options.AddPolicy(AuthorizationPolicies.SectionAccessMedia, policy => - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Media))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Media)); + }); options.AddPolicy(AuthorizationPolicies.SectionAccessMembers, policy => - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Members))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Members)); + }); options.AddPolicy(AuthorizationPolicies.SectionAccessPackages, policy => - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Packages))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Packages)); + }); options.AddPolicy(AuthorizationPolicies.SectionAccessSettings, policy => - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Settings))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Settings)); + }); //We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. options.AddPolicy(AuthorizationPolicies.SectionAccessForContentTree, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); policy.Requirements.Add(new SectionAliasesRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, - Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members))); + Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members)); + }); options.AddPolicy(AuthorizationPolicies.SectionAccessForMediaTree, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); policy.Requirements.Add(new SectionAliasesRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, - Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members))); + Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members)); + }); options.AddPolicy(AuthorizationPolicies.SectionAccessForMemberTree, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); policy.Requirements.Add(new SectionAliasesRequirement( - Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members))); + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); + }); // Permission is granted to this policy if the user has access to any of these sections: Content, media, settings, developer, members options.AddPolicy(AuthorizationPolicies.SectionAccessForDataTypeReading, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); policy.Requirements.Add(new SectionAliasesRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, - Constants.Applications.Settings, Constants.Applications.Packages))); + Constants.Applications.Settings, Constants.Applications.Packages)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessDocuments, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Content))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Content)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessUsers, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Users))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Users)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.PartialViews))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.PartialViews)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViewMacros, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.PartialViewMacros))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.PartialViewMacros)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessPackages, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Packages))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Packages)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessLogs, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.LogViewer))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.LogViewer)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DataTypes))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DataTypes)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessTemplates, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Templates))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Templates)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberTypes))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberTypes)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.RelationTypes))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.RelationTypes)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DocumentTypes))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DocumentTypes)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberGroups))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberGroups)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MediaTypes))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MediaTypes)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessMacros, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Macros))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Macros)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessLanguages, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Languages))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Languages)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessDictionary, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary, Constants.Trees.Dictionary))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary, Constants.Trees.Dictionary)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary, Constants.Trees.Templates))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary, Constants.Trees.Templates)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DocumentTypes, Constants.Trees.Content))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DocumentTypes, Constants.Trees.Content)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MediaTypes, Constants.Trees.Media))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MediaTypes, Constants.Trees.Media)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessMembersOrMemberTypes, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberTypes, Constants.Trees.Members))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberTypes, Constants.Trees.Members)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessAnySchemaTypes, policy => - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes))); + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)); + }); options.AddPolicy(AuthorizationPolicies.TreeAccessAnyContentOrTypes, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); policy.Requirements.Add(new TreeAliasesRequirement( Constants.Trees.DocumentTypes, Constants.Trees.Content, Constants.Trees.MediaTypes, Constants.Trees.Media, - Constants.Trees.MemberTypes, Constants.Trees.Members))); + Constants.Trees.MemberTypes, Constants.Trees.Members)); + }); }); } } diff --git a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs index 1f4abbaa25..94090832bb 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs @@ -1,4 +1,11 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using System; +using Umbraco.Core; +using Umbraco.Core.Hosting; +using Umbraco.Core.Security; +using Umbraco.Extensions; namespace Umbraco.Web.Common.Filters { @@ -33,5 +40,105 @@ namespace Umbraco.Web.Common.Filters { Arguments = new object[] { redirectUrl }; } + + /// + /// Ensures authorization is successful for a back office user. + /// + private class UmbracoBackOfficeAuthorizeFilter : IAuthorizationFilter + { + private readonly bool _requireApproval; + + /// + /// Can be used by unit tests to enable/disable this filter + /// + internal static bool Enable = true; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IUmbracoContextAccessor _umbracoContext; + private readonly IRuntimeState _runtimeState; + private readonly LinkGenerator _linkGenerator; + // TODO: This should really be refactored. Redirection/Challenge is part of Authentication, not part of authorization directly + // We only use this redirectToUmbracoLogin flag in this one instance. I think this + // should be handled as part of the preview authentication process instead. + private readonly bool _redirectToUmbracoLogin; + private string _redirectUrl; + + private UmbracoBackOfficeAuthorizeFilter( + IHostingEnvironment hostingEnvironment, + IUmbracoContextAccessor umbracoContext, + IRuntimeState runtimeState, LinkGenerator linkGenerator, + bool redirectToUmbracoLogin, bool requireApproval, string redirectUrl) + { + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); + _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); + _linkGenerator = linkGenerator ?? throw new ArgumentNullException(nameof(linkGenerator)); + _redirectToUmbracoLogin = redirectToUmbracoLogin; + _redirectUrl = redirectUrl; + _requireApproval = requireApproval; + } + + /// + /// Default constructor + /// + /// + /// + /// + /// + /// + public UmbracoBackOfficeAuthorizeFilter( + IHostingEnvironment hostingEnvironment, + IUmbracoContextAccessor umbracoContext, + IRuntimeState runtimeState, LinkGenerator linkGenerator, + string redirectUrl) : this(hostingEnvironment, umbracoContext, runtimeState, linkGenerator, false, false, redirectUrl) + { + } + + public UmbracoBackOfficeAuthorizeFilter( + IHostingEnvironment hostingEnvironment, + IUmbracoContextAccessor umbracoContext, + IRuntimeState runtimeState, LinkGenerator linkGenerator, + bool redirectToUmbracoLogin, bool requireApproval) : this(hostingEnvironment, umbracoContext, runtimeState, linkGenerator, redirectToUmbracoLogin, requireApproval, null) + { + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!IsAuthorized()) + { + if (_redirectToUmbracoLogin) + { + _redirectUrl = _linkGenerator.GetBackOfficeUrl(_hostingEnvironment); + } + + if (!_redirectUrl.IsNullOrWhiteSpace()) + { + context.Result = new RedirectResult(_redirectUrl); + } + else + { + context.Result = new ForbidResult(); + } + } + } + + private bool IsAuthorized() + { + if (Enable == false) + return true; + + try + { + // if not configured (install or upgrade) then we can continue + // otherwise we need to ensure that a user is logged in + return _runtimeState.Level == RuntimeLevel.Install + || _runtimeState.Level == RuntimeLevel.Upgrade + || _umbracoContext.UmbracoContext?.Security.ValidateCurrentUser(false, _requireApproval) == ValidateRequestAttempt.Success; + } + catch (Exception) + { + return false; + } + } + } } } diff --git a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeFilter.cs deleted file mode 100644 index 05d46b431a..0000000000 --- a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeFilter.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using System; -using Umbraco.Core; -using Umbraco.Core.Security; -using Umbraco.Extensions; -using Umbraco.Web.Security; -using IHostingEnvironment = Umbraco.Core.Hosting.IHostingEnvironment; - -namespace Umbraco.Web.Common.Filters -{ - - /// - /// Ensures authorization is successful for a back office user. - /// - public class UmbracoBackOfficeAuthorizeFilter : IAuthorizationFilter - { - private readonly bool _requireApproval; - - /// - /// Can be used by unit tests to enable/disable this filter - /// - internal static bool Enable = true; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IUmbracoContextAccessor _umbracoContext; - private readonly IRuntimeState _runtimeState; - private readonly LinkGenerator _linkGenerator; - private readonly bool _redirectToUmbracoLogin; - private string _redirectUrl; - - private UmbracoBackOfficeAuthorizeFilter( - IHostingEnvironment hostingEnvironment, - IUmbracoContextAccessor umbracoContext, - IRuntimeState runtimeState, LinkGenerator linkGenerator, - bool redirectToUmbracoLogin, bool requireApproval, string redirectUrl) - { - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); - _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); - _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); - _linkGenerator = linkGenerator ?? throw new ArgumentNullException(nameof(linkGenerator)); - _redirectToUmbracoLogin = redirectToUmbracoLogin; - _redirectUrl = redirectUrl; - _requireApproval = requireApproval; - } - - /// - /// Default constructor - /// - /// - /// - /// - /// - /// - public UmbracoBackOfficeAuthorizeFilter( - IHostingEnvironment hostingEnvironment, - IUmbracoContextAccessor umbracoContext, - IRuntimeState runtimeState, LinkGenerator linkGenerator, - string redirectUrl) : this(hostingEnvironment, umbracoContext, runtimeState, linkGenerator, false, false, redirectUrl) - { - } - - public UmbracoBackOfficeAuthorizeFilter( - IHostingEnvironment hostingEnvironment, - IUmbracoContextAccessor umbracoContext, - IRuntimeState runtimeState, LinkGenerator linkGenerator, - bool redirectToUmbracoLogin, bool requireApproval) : this(hostingEnvironment, umbracoContext, runtimeState, linkGenerator, redirectToUmbracoLogin, requireApproval, null) - { - } - - public void OnAuthorization(AuthorizationFilterContext context) - { - if (!IsAuthorized()) - { - if (_redirectToUmbracoLogin) - { - _redirectUrl = _linkGenerator.GetBackOfficeUrl(_hostingEnvironment); - } - - if (!_redirectUrl.IsNullOrWhiteSpace()) - { - context.Result = new RedirectResult(_redirectUrl); - } - else - { - context.Result = new ForbidResult(); - } - } - } - - private bool IsAuthorized() - { - if (Enable == false) - return true; - - try - { - // if not configured (install or upgrade) then we can continue - // otherwise we need to ensure that a user is logged in - return _runtimeState.Level == RuntimeLevel.Install - || _runtimeState.Level == RuntimeLevel.Upgrade - || _umbracoContext.UmbracoContext?.Security.ValidateCurrentUser(false, _requireApproval) == ValidateRequestAttempt.Success; - } - catch (Exception) - { - return false; - } - } - } -} From 7757447244b6232f66d32adc3f0e9dcfe56f6b67 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Nov 2020 15:32:36 +1100 Subject: [PATCH 09/25] Migrates UmbracoBackOfficeAuthorizeAttribute to authz policies and adds notes/questions --- .../AdminUsersAuthorizeHandler.cs | 3 + .../BackOfficeAuthorizationHandler.cs | 53 ++++ .../BackOfficeAuthorizeRequirement.cs | 17 ++ .../Controllers/AuthenticationController.cs | 10 +- .../Controllers/BackOfficeController.cs | 10 +- .../Controllers/ContentController.cs | 3 +- .../Controllers/CurrentUserController.cs | 7 +- .../Controllers/DashboardController.cs | 4 +- .../Controllers/PreviewController.cs | 10 +- .../UmbracoAuthorizedApiController.cs | 6 +- .../BackOfficeServiceCollectionExtensions.cs | 13 + .../Filters/OverrideAuthorizationAttribute.cs | 2 +- .../OverrideAuthorizationFilterProvider.cs | 1 + .../Authorization/AuthorizationPolicies.cs | 2 + .../UmbracoBackOfficeAuthorizeAttribute.cs | 262 +++++++++--------- .../UmbracoAuthorizedController.cs | 7 +- 16 files changed, 258 insertions(+), 152 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizationHandler.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizeRequirement.cs diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs index 9070246c56..7113fd1b7a 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs @@ -1,14 +1,17 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using System.Linq; using System.Threading.Tasks; using Umbraco.Core; +using Umbraco.Core.Hosting; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Editors; namespace Umbraco.Web.BackOffice.Authorization { + /// /// if the users being edited is an admin then we must ensure that the current user is also an admin /// diff --git a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizationHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizationHandler.cs new file mode 100644 index 0000000000..1efaade3af --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizationHandler.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Authorization; +using System; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Security; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Ensures authorization is successful for a back office user. + /// + public class BackOfficeAuthorizationHandler : AuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backOfficeSecurity; + private readonly IRuntimeState _runtimeState; + + public BackOfficeAuthorizationHandler(IBackOfficeSecurityAccessor backOfficeSecurity, IRuntimeState runtimeState) + { + _backOfficeSecurity = backOfficeSecurity; + _runtimeState = runtimeState; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BackOfficeAuthorizeRequirement requirement) + { + if (!IsAuthorized(requirement)) + { + context.Fail(); + } + else + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + + private bool IsAuthorized(BackOfficeAuthorizeRequirement requirement) + { + try + { + // if not configured (install or upgrade) then we can continue + // otherwise we need to ensure that a user is logged in + return _runtimeState.Level == RuntimeLevel.Install + || _runtimeState.Level == RuntimeLevel.Upgrade + || _backOfficeSecurity.BackOfficeSecurity?.ValidateCurrentUser(false, requirement.RequireApproval) == ValidateRequestAttempt.Success; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizeRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizeRequirement.cs new file mode 100644 index 0000000000..684a90891f --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizeRequirement.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Authorization requirement for the + /// + public class BackOfficeAuthorizeRequirement : IAuthorizationRequirement + { + public BackOfficeAuthorizeRequirement(bool requireApproval = true) + { + RequireApproval = requireApproval; + } + + public bool RequireApproval { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 495adc3c4a..a0ae3868d1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -110,8 +110,8 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Returns the configuration for the backoffice user membership provider - used to configure the change password dialog /// - /// - [UmbracoBackOfficeAuthorize] + /// + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] public IDictionary GetPasswordConfig(int userId) { return _passwordConfiguration.GetConfiguration(userId != _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); @@ -157,7 +157,7 @@ namespace Umbraco.Web.BackOffice.Controllers return _umbracoMapper.Map(user); } - [UmbracoBackOfficeAuthorize] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [ValidateAngularAntiForgeryToken] public async Task PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel) { @@ -242,7 +242,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// is valid before the login screen is displayed. The Auth cookie can be persisted for up to a day but the csrf cookies are only session /// cookies which means that the auth cookie could be valid but the csrf cookies are no longer there, in that case we need to re-set the csrf cookies. /// - [UmbracoBackOfficeAuthorize] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [SetAngularAntiForgeryTokens] //[CheckIfUserTicketDataIsStale] // TODO: Migrate this, though it will need to be done differently at the cookie auth level public UserDetail GetCurrentUser() @@ -264,7 +264,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// We cannot user GetCurrentUser since that requires they are approved, this is the same as GetCurrentUser but doesn't require them to be approved /// - [UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: false, requireApproval: false)] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccessWithoutApproval)] [SetAngularAntiForgeryTokens] [Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)] public ActionResult GetCurrentInvitedUser() diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index d2e7f0d36c..249de5458c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -34,6 +34,8 @@ using Microsoft.AspNetCore.Identity; using System.Security.Claims; using Microsoft.AspNetCore.Http; using Umbraco.Web.Security; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -231,7 +233,7 @@ namespace Umbraco.Web.BackOffice.Controllers return nestedDictionary; } - [UmbracoBackOfficeAuthorize(Order = 0)] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [HttpGet] public IEnumerable GetGridConfig() { @@ -242,7 +244,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// Returns the JavaScript object representing the static server variables javascript object /// /// - [UmbracoBackOfficeAuthorize(Order = 0)] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [MinifyJavaScriptResult(Order = 1)] public async Task ServerVariables() { @@ -278,7 +280,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [UmbracoBackOfficeAuthorize] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [HttpPost] public ActionResult LinkLogin(string provider) { @@ -314,7 +316,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Callback path when the user initiates a link login request from the back office to the external provider from the action /// - [UmbracoBackOfficeAuthorize] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [HttpGet] public async Task ExternalLinkLoginCallback() { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index c22db868b1..a60a773b72 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -130,7 +130,8 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [HttpGet] - [UmbracoBackOfficeAuthorize, OverrideAuthorization] + // TODO: Does this override work? What is best practices for this? + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess), OverrideAuthorization] public bool AllowsCultureVariation() { var contentTypes = _contentTypeService.GetAll(); diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index 2005d42b79..7c984e901e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -27,6 +27,8 @@ using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Common.Filters; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -170,7 +172,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// This only works when the user is logged in (partially) /// - [UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: false, requireApproval : true)] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] // TODO: Why is this necessary? This inherits from UmbracoAuthorizedApiController public async Task PostSetInvitedUserPassword([FromBody]string newPassword) { var user = await _backOfficeUserManager.FindByIdAsync(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString()); @@ -236,7 +238,8 @@ namespace Umbraco.Web.BackOffice.Controllers throw HttpResponseException.CreateValidationErrorResponse(ModelState); } - [UmbracoBackOfficeAuthorize] + // TODO: Why is this necessary? This inherits from UmbracoAuthorizedApiController + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [ValidateAngularAntiForgeryToken] public async Task> GetCurrentUserLinkedLogins() { diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs index 3f797de9c8..ec2642edb7 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -20,6 +20,8 @@ using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Filters; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -28,7 +30,7 @@ namespace Umbraco.Web.BackOffice.Controllers [ValidationFilter] [AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions [IsBackOffice] - [UmbracoBackOfficeAuthorize] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] public class DashboardController : UmbracoApiController { private readonly IUmbracoContextAccessor _umbracoContextAccessor; diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index d83123edc6..40bb8d4122 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -25,6 +25,8 @@ using Umbraco.Web.Services; using Umbraco.Web.Trees; using Umbraco.Web.WebAssets; using Constants = Umbraco.Core.Constants; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -66,8 +68,10 @@ namespace Umbraco.Web.BackOffice.Controllers // TODO: This should really be refactored. Redirection/Challenge is part of Authentication, not part of authorization directly // We only use this redirectToUmbracoLogin flag in this one instance. I think this - // should be handled as part of the preview authentication process instead. - [UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: true, requireApproval : false)] + // should be handled as part of the preview authentication process instead. + // I'm actually not even sure this is required? Wouldn't we automatically redirect to the umbraco login screen anyway here? + //[UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: true, requireApproval : false)] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccessWithoutApproval)] [DisableBrowserCache] public ActionResult Index() { @@ -110,7 +114,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The endpoint that is loaded within the preview iframe /// /// - [UmbracoBackOfficeAuthorize] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] public ActionResult Frame(int id, string culture) { EnterPreview(id); diff --git a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs index 080671d3dd..f1a39e1a76 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs @@ -1,6 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Filters; @@ -16,7 +18,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// [IsBackOffice] [UmbracoUserTimeoutFilter] - [UmbracoBackOfficeAuthorize] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [DisableBrowserCache] [RequireHttps] [CheckIfUserTicketDataIsStale] diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 06e4f1ad4d..81674ca11c 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -120,6 +120,7 @@ namespace Umbraco.Extensions // NOTE: Even though we are registering these handlers globally they will only actually execute their logic for // any auth defining a matching requirement and scheme. + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -128,6 +129,18 @@ namespace Umbraco.Extensions services.AddAuthorization(options => { + options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new BackOfficeAuthorizeRequirement()); + }); + + options.AddPolicy(AuthorizationPolicies.BackOfficeAccessWithoutApproval, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new BackOfficeAuthorizeRequirement(false)); + }); + options.AddPolicy(AuthorizationPolicies.AdminUserEditsRequireAdmin, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); diff --git a/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationAttribute.cs index ed05d831f4..e4aef2a3bc 100644 --- a/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationAttribute.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.BackOffice.Filters /// /// Ensures a special type of authorization filter is ignored. Defaults to . /// - /// The type of authorication filter to override. if null then is used. + /// The type of authorization filter to override. if null then is used. /// /// https://stackoverflow.com/questions/33558095/overrideauthorizationattribute-in-asp-net-5 /// diff --git a/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationFilterProvider.cs b/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationFilterProvider.cs index 6dbf6d747a..9546412270 100644 --- a/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationFilterProvider.cs +++ b/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationFilterProvider.cs @@ -4,6 +4,7 @@ using Umbraco.Core; namespace Umbraco.Web.BackOffice.Filters { + // TODO: Need to figure out if we need this and what we should be doing public class OverrideAuthorizationFilterProvider : IFilterProvider, IFilterMetadata { public void OnProvidersExecuted(FilterProviderContext context) diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index e4add40df9..9f0a6ddd1f 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -7,6 +7,8 @@ { public const string UmbracoFeatureEnabled = nameof(UmbracoFeatureEnabled); + public const string BackOfficeAccess = nameof(BackOfficeAccess); + public const string BackOfficeAccessWithoutApproval = nameof(BackOfficeAccessWithoutApproval); public const string UserBelongsToUserGroupInRequest = nameof(UserBelongsToUserGroupInRequest); public const string AdminUserEditsRequireAdmin = nameof(AdminUserEditsRequireAdmin); public const string DenyLocalLoginIfConfigured = nameof(DenyLocalLoginIfConfigured); diff --git a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs index 94090832bb..1d5a2fc0f1 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs @@ -1,144 +1,144 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; -using System; -using Umbraco.Core; -using Umbraco.Core.Hosting; -using Umbraco.Core.Security; -using Umbraco.Extensions; +//using Microsoft.AspNetCore.Mvc; +//using Microsoft.AspNetCore.Mvc.Filters; +//using Microsoft.AspNetCore.Routing; +//using System; +//using Umbraco.Core; +//using Umbraco.Core.Hosting; +//using Umbraco.Core.Security; +//using Umbraco.Extensions; -namespace Umbraco.Web.Common.Filters -{ - /// - /// Ensures authorization is successful for a back office user. - /// - public class UmbracoBackOfficeAuthorizeAttribute : TypeFilterAttribute - { - /// - /// Default constructor - /// - public UmbracoBackOfficeAuthorizeAttribute() : this(false, false) - { - } +//namespace Umbraco.Web.Common.Filters +//{ +// /// +// /// Ensures authorization is successful for a back office user. +// /// +// public class UmbracoBackOfficeAuthorizeAttribute : TypeFilterAttribute +// { +// /// +// /// Default constructor +// /// +// public UmbracoBackOfficeAuthorizeAttribute() : this(false, false) +// { +// } - /// - /// Constructor with redirect umbraco login behavior - /// - /// - /// +// /// +// /// Constructor with redirect umbraco login behavior +// /// +// /// +// /// - public UmbracoBackOfficeAuthorizeAttribute(bool redirectToUmbracoLogin, bool requireApproval) : base(typeof(UmbracoBackOfficeAuthorizeFilter)) - { - Arguments = new object[] { redirectToUmbracoLogin, requireApproval }; - } +// public UmbracoBackOfficeAuthorizeAttribute(bool redirectToUmbracoLogin, bool requireApproval) : base(typeof(UmbracoBackOfficeAuthorizeFilter)) +// { +// Arguments = new object[] { redirectToUmbracoLogin, requireApproval }; +// } - /// - /// Constructor with redirect url behavior - /// - /// - public UmbracoBackOfficeAuthorizeAttribute(string redirectUrl) : base(typeof(UmbracoBackOfficeAuthorizeFilter)) - { - Arguments = new object[] { redirectUrl }; - } +// /// +// /// Constructor with redirect url behavior +// /// +// /// +// public UmbracoBackOfficeAuthorizeAttribute(string redirectUrl) : base(typeof(UmbracoBackOfficeAuthorizeFilter)) +// { +// Arguments = new object[] { redirectUrl }; +// } - /// - /// Ensures authorization is successful for a back office user. - /// - private class UmbracoBackOfficeAuthorizeFilter : IAuthorizationFilter - { - private readonly bool _requireApproval; +// /// +// /// Ensures authorization is successful for a back office user. +// /// +// private class UmbracoBackOfficeAuthorizeFilter : IAuthorizationFilter +// { +// private readonly bool _requireApproval; - /// - /// Can be used by unit tests to enable/disable this filter - /// - internal static bool Enable = true; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IUmbracoContextAccessor _umbracoContext; - private readonly IRuntimeState _runtimeState; - private readonly LinkGenerator _linkGenerator; - // TODO: This should really be refactored. Redirection/Challenge is part of Authentication, not part of authorization directly - // We only use this redirectToUmbracoLogin flag in this one instance. I think this - // should be handled as part of the preview authentication process instead. - private readonly bool _redirectToUmbracoLogin; - private string _redirectUrl; +// /// +// /// Can be used by unit tests to enable/disable this filter +// /// +// internal static bool Enable = true; +// private readonly IHostingEnvironment _hostingEnvironment; +// private readonly IUmbracoContextAccessor _umbracoContext; +// private readonly IRuntimeState _runtimeState; +// private readonly LinkGenerator _linkGenerator; +// // TODO: This should really be refactored. Redirection/Challenge is part of Authentication, not part of authorization directly +// // We only use this redirectToUmbracoLogin flag in this one instance. I think this +// // should be handled as part of the preview authentication process instead. +// private readonly bool _redirectToUmbracoLogin; +// private string _redirectUrl; - private UmbracoBackOfficeAuthorizeFilter( - IHostingEnvironment hostingEnvironment, - IUmbracoContextAccessor umbracoContext, - IRuntimeState runtimeState, LinkGenerator linkGenerator, - bool redirectToUmbracoLogin, bool requireApproval, string redirectUrl) - { - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); - _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); - _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); - _linkGenerator = linkGenerator ?? throw new ArgumentNullException(nameof(linkGenerator)); - _redirectToUmbracoLogin = redirectToUmbracoLogin; - _redirectUrl = redirectUrl; - _requireApproval = requireApproval; - } +// private UmbracoBackOfficeAuthorizeFilter( +// IHostingEnvironment hostingEnvironment, +// IUmbracoContextAccessor umbracoContext, +// IRuntimeState runtimeState, LinkGenerator linkGenerator, +// bool redirectToUmbracoLogin, bool requireApproval, string redirectUrl) +// { +// _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); +// _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); +// _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); +// _linkGenerator = linkGenerator ?? throw new ArgumentNullException(nameof(linkGenerator)); +// _redirectToUmbracoLogin = redirectToUmbracoLogin; +// _redirectUrl = redirectUrl; +// _requireApproval = requireApproval; +// } - /// - /// Default constructor - /// - /// - /// - /// - /// - /// - public UmbracoBackOfficeAuthorizeFilter( - IHostingEnvironment hostingEnvironment, - IUmbracoContextAccessor umbracoContext, - IRuntimeState runtimeState, LinkGenerator linkGenerator, - string redirectUrl) : this(hostingEnvironment, umbracoContext, runtimeState, linkGenerator, false, false, redirectUrl) - { - } +// /// +// /// Default constructor +// /// +// /// +// /// +// /// +// /// +// /// +// public UmbracoBackOfficeAuthorizeFilter( +// IHostingEnvironment hostingEnvironment, +// IUmbracoContextAccessor umbracoContext, +// IRuntimeState runtimeState, LinkGenerator linkGenerator, +// string redirectUrl) : this(hostingEnvironment, umbracoContext, runtimeState, linkGenerator, false, false, redirectUrl) +// { +// } - public UmbracoBackOfficeAuthorizeFilter( - IHostingEnvironment hostingEnvironment, - IUmbracoContextAccessor umbracoContext, - IRuntimeState runtimeState, LinkGenerator linkGenerator, - bool redirectToUmbracoLogin, bool requireApproval) : this(hostingEnvironment, umbracoContext, runtimeState, linkGenerator, redirectToUmbracoLogin, requireApproval, null) - { - } +// public UmbracoBackOfficeAuthorizeFilter( +// IHostingEnvironment hostingEnvironment, +// IUmbracoContextAccessor umbracoContext, +// IRuntimeState runtimeState, LinkGenerator linkGenerator, +// bool redirectToUmbracoLogin, bool requireApproval) : this(hostingEnvironment, umbracoContext, runtimeState, linkGenerator, redirectToUmbracoLogin, requireApproval, null) +// { +// } - public void OnAuthorization(AuthorizationFilterContext context) - { - if (!IsAuthorized()) - { - if (_redirectToUmbracoLogin) - { - _redirectUrl = _linkGenerator.GetBackOfficeUrl(_hostingEnvironment); - } +// public void OnAuthorization(AuthorizationFilterContext context) +// { +// if (!IsAuthorized()) +// { +// if (_redirectToUmbracoLogin) +// { +// _redirectUrl = _linkGenerator.GetBackOfficeUrl(_hostingEnvironment); +// } - if (!_redirectUrl.IsNullOrWhiteSpace()) - { - context.Result = new RedirectResult(_redirectUrl); - } - else - { - context.Result = new ForbidResult(); - } - } - } +// if (!_redirectUrl.IsNullOrWhiteSpace()) +// { +// context.Result = new RedirectResult(_redirectUrl); +// } +// else +// { +// context.Result = new ForbidResult(); +// } +// } +// } - private bool IsAuthorized() - { - if (Enable == false) - return true; +// private bool IsAuthorized() +// { +// if (Enable == false) +// return true; - try - { - // if not configured (install or upgrade) then we can continue - // otherwise we need to ensure that a user is logged in - return _runtimeState.Level == RuntimeLevel.Install - || _runtimeState.Level == RuntimeLevel.Upgrade - || _umbracoContext.UmbracoContext?.Security.ValidateCurrentUser(false, _requireApproval) == ValidateRequestAttempt.Success; - } - catch (Exception) - { - return false; - } - } - } - } -} +// try +// { +// // if not configured (install or upgrade) then we can continue +// // otherwise we need to ensure that a user is logged in +// return _runtimeState.Level == RuntimeLevel.Install +// || _runtimeState.Level == RuntimeLevel.Upgrade +// || _umbracoContext.UmbracoContext?.Security.ValidateCurrentUser(false, _requireApproval) == ValidateRequestAttempt.Success; +// } +// catch (Exception) +// { +// return false; +// } +// } +// } +// } +//} diff --git a/src/Umbraco.Web.Website/Controllers/UmbracoAuthorizedController.cs b/src/Umbraco.Web.Website/Controllers/UmbracoAuthorizedController.cs index b2611848df..ad51d9cb4f 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbracoAuthorizedController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbracoAuthorizedController.cs @@ -1,4 +1,6 @@ -using Umbraco.Web.Common.Filters; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; +using Umbraco.Web.Common.Filters; namespace Umbraco.Web.Mvc { @@ -9,10 +11,11 @@ namespace Umbraco.Web.Mvc /// This controller essentially just uses a global UmbracoAuthorizeAttribute, inheritors that require more granular control over the /// authorization of each method can use this attribute instead of inheriting from this controller. /// - [UmbracoBackOfficeAuthorize] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [DisableBrowserCache] public abstract class UmbracoAuthorizedController : UmbracoController { + // TODO: This controller is not used at all, is there a need for this controller? } } From eeb6c79b14582dd7bf44ec39367966f071242c48 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Nov 2020 15:37:18 +1100 Subject: [PATCH 10/25] After testing, removes notes --- .../Controllers/PreviewController.cs | 5 - .../UmbracoBackOfficeAuthorizeAttribute.cs | 144 ------------------ 2 files changed, 149 deletions(-) delete mode 100644 src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index 40bb8d4122..dc565e7e5c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -66,11 +66,6 @@ namespace Umbraco.Web.BackOffice.Controllers _viewEngines = viewEngines; } - // TODO: This should really be refactored. Redirection/Challenge is part of Authentication, not part of authorization directly - // We only use this redirectToUmbracoLogin flag in this one instance. I think this - // should be handled as part of the preview authentication process instead. - // I'm actually not even sure this is required? Wouldn't we automatically redirect to the umbraco login screen anyway here? - //[UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: true, requireApproval : false)] [Authorize(Policy = AuthorizationPolicies.BackOfficeAccessWithoutApproval)] [DisableBrowserCache] public ActionResult Index() diff --git a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs deleted file mode 100644 index 1d5a2fc0f1..0000000000 --- a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs +++ /dev/null @@ -1,144 +0,0 @@ -//using Microsoft.AspNetCore.Mvc; -//using Microsoft.AspNetCore.Mvc.Filters; -//using Microsoft.AspNetCore.Routing; -//using System; -//using Umbraco.Core; -//using Umbraco.Core.Hosting; -//using Umbraco.Core.Security; -//using Umbraco.Extensions; - -//namespace Umbraco.Web.Common.Filters -//{ -// /// -// /// Ensures authorization is successful for a back office user. -// /// -// public class UmbracoBackOfficeAuthorizeAttribute : TypeFilterAttribute -// { -// /// -// /// Default constructor -// /// -// public UmbracoBackOfficeAuthorizeAttribute() : this(false, false) -// { -// } - -// /// -// /// Constructor with redirect umbraco login behavior -// /// -// /// -// /// - -// public UmbracoBackOfficeAuthorizeAttribute(bool redirectToUmbracoLogin, bool requireApproval) : base(typeof(UmbracoBackOfficeAuthorizeFilter)) -// { -// Arguments = new object[] { redirectToUmbracoLogin, requireApproval }; -// } - -// /// -// /// Constructor with redirect url behavior -// /// -// /// -// public UmbracoBackOfficeAuthorizeAttribute(string redirectUrl) : base(typeof(UmbracoBackOfficeAuthorizeFilter)) -// { -// Arguments = new object[] { redirectUrl }; -// } - -// /// -// /// Ensures authorization is successful for a back office user. -// /// -// private class UmbracoBackOfficeAuthorizeFilter : IAuthorizationFilter -// { -// private readonly bool _requireApproval; - -// /// -// /// Can be used by unit tests to enable/disable this filter -// /// -// internal static bool Enable = true; -// private readonly IHostingEnvironment _hostingEnvironment; -// private readonly IUmbracoContextAccessor _umbracoContext; -// private readonly IRuntimeState _runtimeState; -// private readonly LinkGenerator _linkGenerator; -// // TODO: This should really be refactored. Redirection/Challenge is part of Authentication, not part of authorization directly -// // We only use this redirectToUmbracoLogin flag in this one instance. I think this -// // should be handled as part of the preview authentication process instead. -// private readonly bool _redirectToUmbracoLogin; -// private string _redirectUrl; - -// private UmbracoBackOfficeAuthorizeFilter( -// IHostingEnvironment hostingEnvironment, -// IUmbracoContextAccessor umbracoContext, -// IRuntimeState runtimeState, LinkGenerator linkGenerator, -// bool redirectToUmbracoLogin, bool requireApproval, string redirectUrl) -// { -// _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); -// _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); -// _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); -// _linkGenerator = linkGenerator ?? throw new ArgumentNullException(nameof(linkGenerator)); -// _redirectToUmbracoLogin = redirectToUmbracoLogin; -// _redirectUrl = redirectUrl; -// _requireApproval = requireApproval; -// } - -// /// -// /// Default constructor -// /// -// /// -// /// -// /// -// /// -// /// -// public UmbracoBackOfficeAuthorizeFilter( -// IHostingEnvironment hostingEnvironment, -// IUmbracoContextAccessor umbracoContext, -// IRuntimeState runtimeState, LinkGenerator linkGenerator, -// string redirectUrl) : this(hostingEnvironment, umbracoContext, runtimeState, linkGenerator, false, false, redirectUrl) -// { -// } - -// public UmbracoBackOfficeAuthorizeFilter( -// IHostingEnvironment hostingEnvironment, -// IUmbracoContextAccessor umbracoContext, -// IRuntimeState runtimeState, LinkGenerator linkGenerator, -// bool redirectToUmbracoLogin, bool requireApproval) : this(hostingEnvironment, umbracoContext, runtimeState, linkGenerator, redirectToUmbracoLogin, requireApproval, null) -// { -// } - -// public void OnAuthorization(AuthorizationFilterContext context) -// { -// if (!IsAuthorized()) -// { -// if (_redirectToUmbracoLogin) -// { -// _redirectUrl = _linkGenerator.GetBackOfficeUrl(_hostingEnvironment); -// } - -// if (!_redirectUrl.IsNullOrWhiteSpace()) -// { -// context.Result = new RedirectResult(_redirectUrl); -// } -// else -// { -// context.Result = new ForbidResult(); -// } -// } -// } - -// private bool IsAuthorized() -// { -// if (Enable == false) -// return true; - -// try -// { -// // if not configured (install or upgrade) then we can continue -// // otherwise we need to ensure that a user is logged in -// return _runtimeState.Level == RuntimeLevel.Install -// || _runtimeState.Level == RuntimeLevel.Upgrade -// || _umbracoContext.UmbracoContext?.Security.ValidateCurrentUser(false, _requireApproval) == ValidateRequestAttempt.Success; -// } -// catch (Exception) -// { -// return false; -// } -// } -// } -// } -//} From 65a11a4e2644b4a5e0ed4c63b295a54ee25b6f06 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Nov 2020 15:40:20 +1100 Subject: [PATCH 11/25] Adds notes --- .../Filters/UmbracoMemberAuthorizeFilter.cs | 4 +++- .../Filters/ValidateUmbracoFormRouteStringAttribute.cs | 2 ++ src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs index 27c2922637..7f7dcde008 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs @@ -8,10 +8,12 @@ namespace Umbraco.Web.Common.Filters { /// - /// Ensures authorization is successful for a back office user. + /// Ensures authorization is successful for a front-end member /// public class UmbracoMemberAuthorizeFilter : IAuthorizationFilter { + // TODO: Lets revisit this when we get members done and the front-end working and whether it can be replaced or moved to an authz policy + /// /// Comma delimited list of allowed member types /// diff --git a/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs b/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs index 45806b9d18..bbd3aa981e 100644 --- a/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs @@ -22,6 +22,8 @@ namespace Umbraco.Web.Common.Filters public class ValidateUmbracoFormRouteStringAttribute : TypeFilterAttribute { + // TODO: Lets revisit this when we get members done and the front-end working and whether it can moved to an authz policy + public ValidateUmbracoFormRouteStringAttribute() : base(typeof(ValidateUmbracoFormRouteStringFilter)) { Arguments = new object[] { }; diff --git a/src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs index 6ccebc9b25..5490da961d 100644 --- a/src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs +++ b/src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs @@ -13,6 +13,8 @@ namespace Umbraco.Web.Common.Install /// public class InstallAuthorizeAttribute : TypeFilterAttribute { + // NOTE: This doesn't need to be an authz policy, it's only used for the installer + public InstallAuthorizeAttribute() : base(typeof(InstallAuthorizeFilter)) { } From c34540cb0653d4568bb3281060d64eabe3d7bab5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 23 Nov 2020 22:43:41 +1100 Subject: [PATCH 12/25] Migrates EnsureUserPermissionForContentAttribute to authz policies --- src/Umbraco.Core/Actions/ActionCopy.cs | 4 +- src/Umbraco.Core/Actions/ActionRights.cs | 4 +- src/Umbraco.Core/Actions/ActionSort.cs | 4 +- src/Umbraco.Core/Actions/ActionUnpublish.cs | 4 +- ...thorizeHandler.cs => AdminUsersHandler.cs} | 10 +- ...equirement.cs => AdminUsersRequirement.cs} | 6 +- ...izationHandler.cs => BackOfficeHandler.cs} | 8 +- ...equirement.cs => BackOfficeRequirement.cs} | 6 +- .../ContentPermissionQueryStringHandler.cs | 154 +++++++++++ .../ContentPermissionResourceRequirement.cs | 21 ++ ...ontentPermissionsQueryStringRequirement.cs | 38 +++ ...izeHandler.cs => DenyLocalLoginHandler.cs} | 4 +- .../DenyLocalLoginRequirement.cs | 2 +- ...nAuthorizeHandler.cs => SectionHandler.cs} | 8 +- ...esRequirement.cs => SectionRequirement.cs} | 6 +- ...TreeAuthorizeHandler.cs => TreeHandler.cs} | 8 +- ...iasesRequirement.cs => TreeRequirement.cs} | 6 +- ...rizationHandler.cs => UserGroupHandler.cs} | 8 +- ...Requirement.cs => UserGroupRequirement.cs} | 6 +- .../Controllers/ContentController.cs | 100 ++++--- .../BackOfficeServiceCollectionExtensions.cs | 142 ++++++---- ...EnsureUserPermissionForContentAttribute.cs | 252 ------------------ .../Authorization/AuthorizationPolicies.cs | 10 + .../Authorization/FeatureAuthorizeHandler.cs | 11 +- 24 files changed, 443 insertions(+), 379 deletions(-) rename src/Umbraco.Web.BackOffice/Authorization/{AdminUsersAuthorizeHandler.cs => AdminUsersHandler.cs} (86%) rename src/Umbraco.Web.BackOffice/Authorization/{AdminUsersAuthorizeRequirement.cs => AdminUsersRequirement.cs} (52%) rename src/Umbraco.Web.BackOffice/Authorization/{BackOfficeAuthorizationHandler.cs => BackOfficeHandler.cs} (79%) rename src/Umbraco.Web.BackOffice/Authorization/{BackOfficeAuthorizeRequirement.cs => BackOfficeRequirement.cs} (52%) create mode 100644 src/Umbraco.Web.BackOffice/Authorization/ContentPermissionQueryStringHandler.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceRequirement.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs rename src/Umbraco.Web.BackOffice/Authorization/{DenyLocalLoginAuthorizeHandler.cs => DenyLocalLoginHandler.cs} (81%) rename src/Umbraco.Web.BackOffice/Authorization/{UmbracoSectionAuthorizeHandler.cs => SectionHandler.cs} (79%) rename src/Umbraco.Web.BackOffice/Authorization/{SectionAliasesRequirement.cs => SectionRequirement.cs} (59%) rename src/Umbraco.Web.BackOffice/Authorization/{UmbracoTreeAuthorizeHandler.cs => TreeHandler.cs} (86%) rename src/Umbraco.Web.BackOffice/Authorization/{TreeAliasesRequirement.cs => TreeRequirement.cs} (60%) rename src/Umbraco.Web.BackOffice/Authorization/{UserGroupAuthorizationHandler.cs => UserGroupHandler.cs} (88%) rename src/Umbraco.Web.BackOffice/Authorization/{UserGroupAuthorizeRequirement.cs => UserGroupRequirement.cs} (52%) delete mode 100644 src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForContentAttribute.cs diff --git a/src/Umbraco.Core/Actions/ActionCopy.cs b/src/Umbraco.Core/Actions/ActionCopy.cs index 477156d4ae..a568d0aa37 100644 --- a/src/Umbraco.Core/Actions/ActionCopy.cs +++ b/src/Umbraco.Core/Actions/ActionCopy.cs @@ -7,7 +7,9 @@ namespace Umbraco.Web.Actions /// public class ActionCopy : IAction { - public char Letter => 'O'; + public const char ActionLetter = 'O'; + + public char Letter => ActionLetter; public string Alias => "copy"; public string Category => Constants.Conventions.PermissionCategories.StructureCategory; public string Icon => "documents"; diff --git a/src/Umbraco.Core/Actions/ActionRights.cs b/src/Umbraco.Core/Actions/ActionRights.cs index b76f3b7800..dd021d03c0 100644 --- a/src/Umbraco.Core/Actions/ActionRights.cs +++ b/src/Umbraco.Core/Actions/ActionRights.cs @@ -7,7 +7,9 @@ namespace Umbraco.Web.Actions /// public class ActionRights : IAction { - public char Letter => 'R'; + public const char ActionLetter = 'R'; + + public char Letter => ActionLetter; public string Alias => "rights"; public string Category => Constants.Conventions.PermissionCategories.ContentCategory; public string Icon => "vcard"; diff --git a/src/Umbraco.Core/Actions/ActionSort.cs b/src/Umbraco.Core/Actions/ActionSort.cs index 8b4e01823e..9c463bc18e 100644 --- a/src/Umbraco.Core/Actions/ActionSort.cs +++ b/src/Umbraco.Core/Actions/ActionSort.cs @@ -9,7 +9,9 @@ namespace Umbraco.Web.Actions /// public class ActionSort : IAction { - public char Letter => 'S'; + public const char ActionLetter = 'S'; + + public char Letter => ActionLetter; public string Alias => "sort"; public string Category => Constants.Conventions.PermissionCategories.StructureCategory; public string Icon => "navigation-vertical"; diff --git a/src/Umbraco.Core/Actions/ActionUnpublish.cs b/src/Umbraco.Core/Actions/ActionUnpublish.cs index 8ece4c008e..f9270d926b 100644 --- a/src/Umbraco.Core/Actions/ActionUnpublish.cs +++ b/src/Umbraco.Core/Actions/ActionUnpublish.cs @@ -10,7 +10,9 @@ namespace Umbraco.Web.Actions /// public class ActionUnpublish : IAction { - public char Letter => 'Z'; + public const char ActionLetter = 'Z'; + + public char Letter => ActionLetter; public string Alias => "unpublish"; public string Category => Constants.Conventions.PermissionCategories.ContentCategory; public string Icon => "circle-dotted"; diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs similarity index 86% rename from src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs rename to src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs index 7113fd1b7a..05c18b4bb6 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs @@ -1,10 +1,8 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; using System.Linq; using System.Threading.Tasks; using Umbraco.Core; -using Umbraco.Core.Hosting; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Editors; @@ -15,14 +13,14 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// if the users being edited is an admin then we must ensure that the current user is also an admin /// - public class AdminUsersAuthorizeHandler : AuthorizationHandler + public class AdminUsersHandler : AuthorizationHandler { private readonly IHttpContextAccessor _httpContextAcessor; private readonly IUserService _userService; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly UserEditorAuthorizationHelper _userEditorAuthorizationHelper; - public AdminUsersAuthorizeHandler(IHttpContextAccessor httpContextAcessor, + public AdminUsersHandler(IHttpContextAccessor httpContextAcessor, IUserService userService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, UserEditorAuthorizationHelper userEditorAuthorizationHelper) @@ -33,7 +31,7 @@ namespace Umbraco.Web.BackOffice.Authorization _userEditorAuthorizationHelper = userEditorAuthorizationHelper; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminUsersAuthorizeRequirement requirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminUsersRequirement requirement) { var isAuth = IsAuthorized(requirement); if (!isAuth.HasValue || isAuth.Value) @@ -48,7 +46,7 @@ namespace Umbraco.Web.BackOffice.Authorization return Task.CompletedTask; } - private bool? IsAuthorized(AdminUsersAuthorizeRequirement requirement) + private bool? IsAuthorized(AdminUsersRequirement requirement) { int[] userIds; diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersRequirement.cs similarity index 52% rename from src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeRequirement.cs rename to src/Umbraco.Web.BackOffice/Authorization/AdminUsersRequirement.cs index a4fec9c729..3d168776e9 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersAuthorizeRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersRequirement.cs @@ -4,11 +4,11 @@ namespace Umbraco.Web.BackOffice.Authorization { /// - /// Authorization requirement for the + /// Authorization requirement for the /// - public class AdminUsersAuthorizeRequirement : IAuthorizationRequirement + public class AdminUsersRequirement : IAuthorizationRequirement { - public AdminUsersAuthorizeRequirement(string queryStringName = "id") + public AdminUsersRequirement(string queryStringName = "id") { QueryStringName = queryStringName; } diff --git a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizationHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs similarity index 79% rename from src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizationHandler.cs rename to src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs index 1efaade3af..8c1da3b204 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizationHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs @@ -9,18 +9,18 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Ensures authorization is successful for a back office user. /// - public class BackOfficeAuthorizationHandler : AuthorizationHandler + public class BackOfficeHandler : AuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backOfficeSecurity; private readonly IRuntimeState _runtimeState; - public BackOfficeAuthorizationHandler(IBackOfficeSecurityAccessor backOfficeSecurity, IRuntimeState runtimeState) + public BackOfficeHandler(IBackOfficeSecurityAccessor backOfficeSecurity, IRuntimeState runtimeState) { _backOfficeSecurity = backOfficeSecurity; _runtimeState = runtimeState; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BackOfficeAuthorizeRequirement requirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BackOfficeRequirement requirement) { if (!IsAuthorized(requirement)) { @@ -34,7 +34,7 @@ namespace Umbraco.Web.BackOffice.Authorization return Task.CompletedTask; } - private bool IsAuthorized(BackOfficeAuthorizeRequirement requirement) + private bool IsAuthorized(BackOfficeRequirement requirement) { try { diff --git a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizeRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeRequirement.cs similarity index 52% rename from src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizeRequirement.cs rename to src/Umbraco.Web.BackOffice/Authorization/BackOfficeRequirement.cs index 684a90891f..d1b13efe2c 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeAuthorizeRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeRequirement.cs @@ -3,11 +3,11 @@ namespace Umbraco.Web.BackOffice.Authorization { /// - /// Authorization requirement for the + /// Authorization requirement for the /// - public class BackOfficeAuthorizeRequirement : IAuthorizationRequirement + public class BackOfficeRequirement : IAuthorizationRequirement { - public BackOfficeAuthorizeRequirement(bool requireApproval = true) + public BackOfficeRequirement(bool requireApproval = true) { RequireApproval = requireApproval; } diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionQueryStringHandler.cs new file mode 100644 index 0000000000..b6e99d9320 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionQueryStringHandler.cs @@ -0,0 +1,154 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.Primitives; +using System; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Security; +using Umbraco.Core.Services; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Used to authorize if the user has the correct permission access to the content for the specified + /// + public class ContentPermissionResourceHandler : AuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly IEntityService _entityService; + private readonly IUserService _userService; + + public ContentPermissionResourceHandler( + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IEntityService entityService, + IUserService userService) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _entityService = entityService; + _userService = userService; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionResourceRequirement requirement, IContent resource) + { + var permissionResult = ContentPermissionsHelper.CheckPermissions(resource, + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + _userService, + _entityService, + new[] { requirement.PermissionToCheck }); + + if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) + { + context.Fail(); + } + else + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } + + /// + /// Used to authorize if the user has the correct permission access to the content for the content id specified in a query string + /// + public class ContentPermissionQueryStringHandler : AuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IEntityService _entityService; + private readonly IUserService _userService; + private readonly IContentService _contentService; + + public ContentPermissionQueryStringHandler( + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IHttpContextAccessor httpContextAccessor, + IEntityService entityService, + IUserService userService, + IContentService contentService) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _httpContextAccessor = httpContextAccessor; + _entityService = entityService; + _userService = userService; + _contentService = contentService; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionsQueryStringRequirement requirement) + { + int nodeId; + if (requirement.NodeId.HasValue == false) + { + StringValues routeVal; + foreach(var qs in requirement.QueryStringNames) + { + if (_httpContextAccessor.HttpContext.Request.Query.TryGetValue(qs, out routeVal)) + { + break; + } + } + + if (routeVal.Count == 0) + { + throw new InvalidOperationException("No argument found for the current action with by names " + string.Join(", ", requirement.QueryStringNames)); + } + + var argument = routeVal.ToString(); + // if the argument is an int, it will parse and can be assigned to nodeId + // if might be a udi, so check that next + // otherwise treat it as a guid - unlikely we ever get here + if (int.TryParse(argument, out int parsedId)) + { + nodeId = parsedId; + } + else if (UdiParser.TryParse(argument, true, out var udi)) + { + nodeId = _entityService.GetId(udi).Result; + } + else + { + Guid.TryParse(argument, out Guid key); + nodeId = _entityService.GetId(key, UmbracoObjectTypes.Document).Result; + } + } + else + { + nodeId = requirement.NodeId.Value; + } + + var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + _userService, + _contentService, + _entityService, + out var contentItem, + new[] { requirement.PermissionToCheck }); + + if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound) + { + return null; + } + + if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) + { + context.Fail(); + } + else + { + context.Succeed(requirement); + } + + + if (contentItem != null) + { + //store the content item in request cache so it can be resolved in the controller without re-looking it up + _httpContextAccessor.HttpContext.Items[typeof(IContent).ToString()] = contentItem; + } + + return Task.CompletedTask; + } + + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceRequirement.cs new file mode 100644 index 0000000000..6781f45ab5 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceRequirement.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// An authorization requirement for + /// + public class ContentPermissionResourceRequirement : IAuthorizationRequirement + { + /// + /// Create an authorization requirement for a resource + /// + /// + public ContentPermissionResourceRequirement(char permissionToCheck) + { + PermissionToCheck = permissionToCheck; + } + + public char PermissionToCheck { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs new file mode 100644 index 0000000000..9d2e931495 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + + /// + /// An authorization requirement for + /// + public class ContentPermissionsQueryStringRequirement : IAuthorizationRequirement + { + + /// + /// Create an authorization requirement for a specific node id + /// + /// + /// + public ContentPermissionsQueryStringRequirement(int nodeId, char permissionToCheck) + { + NodeId = nodeId; + PermissionToCheck = permissionToCheck; + } + + /// + /// Create an authorization requirement for a node id based on a query string parameter + /// + /// + /// + public ContentPermissionsQueryStringRequirement(char permissionToCheck, string[] paramNames) + { + QueryStringNames = paramNames; + PermissionToCheck = permissionToCheck; + } + + public int? NodeId { get; } + public string[] QueryStringNames { get; } + public char PermissionToCheck { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginAuthorizeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs similarity index 81% rename from src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginAuthorizeHandler.cs rename to src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs index 510682081a..70656e202f 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginAuthorizeHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs @@ -8,11 +8,11 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Ensures the resource cannot be accessed if returns true /// - public class DenyLocalLoginAuthorizeHandler : AuthorizationHandler + public class DenyLocalLoginHandler : AuthorizationHandler { private readonly IBackOfficeExternalLoginProviders _externalLogins; - public DenyLocalLoginAuthorizeHandler(IBackOfficeExternalLoginProviders externalLogins) + public DenyLocalLoginHandler(IBackOfficeExternalLoginProviders externalLogins) { _externalLogins = externalLogins; } diff --git a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginRequirement.cs index 5fe7f1c071..a4f3a7e306 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginRequirement.cs @@ -3,7 +3,7 @@ namespace Umbraco.Web.BackOffice.Authorization { /// - /// Marker requirement for the + /// Marker requirement for the /// public class DenyLocalLoginRequirement : IAuthorizationRequirement { diff --git a/src/Umbraco.Web.BackOffice/Authorization/UmbracoSectionAuthorizeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/SectionHandler.cs similarity index 79% rename from src/Umbraco.Web.BackOffice/Authorization/UmbracoSectionAuthorizeHandler.cs rename to src/Umbraco.Web.BackOffice/Authorization/SectionHandler.cs index b063c4c7d9..91ed79c685 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/UmbracoSectionAuthorizeHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/SectionHandler.cs @@ -12,16 +12,16 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// The user only needs access to one of the sections specified, not all of the sections. /// - public class UmbracoSectionAuthorizeHandler : AuthorizationHandler + public class SectionHandler : AuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - public UmbracoSectionAuthorizeHandler(IBackOfficeSecurityAccessor backofficeSecurityAccessor) + public SectionHandler(IBackOfficeSecurityAccessor backofficeSecurityAccessor) { _backofficeSecurityAccessor = backofficeSecurityAccessor; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SectionAliasesRequirement requirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SectionRequirement requirement) { if (IsAuthorized(requirement)) { @@ -35,7 +35,7 @@ namespace Umbraco.Web.BackOffice.Authorization return Task.CompletedTask; } - private bool IsAuthorized(SectionAliasesRequirement requirement) + private bool IsAuthorized(SectionRequirement requirement) { var authorized = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null && requirement.SectionAliases.Any(app => _backofficeSecurityAccessor.BackOfficeSecurity.UserHasSectionAccess( diff --git a/src/Umbraco.Web.BackOffice/Authorization/SectionAliasesRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/SectionRequirement.cs similarity index 59% rename from src/Umbraco.Web.BackOffice/Authorization/SectionAliasesRequirement.cs rename to src/Umbraco.Web.BackOffice/Authorization/SectionRequirement.cs index 9789258bf8..033eccdd18 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/SectionAliasesRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/SectionRequirement.cs @@ -4,15 +4,15 @@ using System.Collections.Generic; namespace Umbraco.Web.BackOffice.Authorization { /// - /// Authorization requirements for + /// Authorization requirements for /// - public class SectionAliasesRequirement : IAuthorizationRequirement + public class SectionRequirement : IAuthorizationRequirement { /// /// The aliases for sections that the user will need access to /// public IReadOnlyCollection SectionAliases { get; } - public SectionAliasesRequirement(params string[] aliases) => SectionAliases = aliases; + public SectionRequirement(params string[] aliases) => SectionAliases = aliases; } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/UmbracoTreeAuthorizeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/TreeHandler.cs similarity index 86% rename from src/Umbraco.Web.BackOffice/Authorization/UmbracoTreeAuthorizeHandler.cs rename to src/Umbraco.Web.BackOffice/Authorization/TreeHandler.cs index 9be8fd578f..932f467ac4 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/UmbracoTreeAuthorizeHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/TreeHandler.cs @@ -16,7 +16,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// This would allow a tree to be moved between sections. /// The user only needs access to one of the trees specified, not all of the trees. /// - public class UmbracoTreeAuthorizeHandler : AuthorizationHandler + public class TreeHandler : AuthorizationHandler { private readonly ITreeService _treeService; @@ -31,13 +31,13 @@ namespace Umbraco.Web.BackOffice.Authorization /// If the user has access to the application that the treeAlias is specified in, they will be authorized. /// Multiple trees may be specified. /// - public UmbracoTreeAuthorizeHandler(ITreeService treeService, IBackOfficeSecurityAccessor backofficeSecurityAccessor) + public TreeHandler(ITreeService treeService, IBackOfficeSecurityAccessor backofficeSecurityAccessor) { _treeService = treeService ?? throw new ArgumentNullException(nameof(treeService)); _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TreeAliasesRequirement requirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TreeRequirement requirement) { if (IsAuthorized(requirement)) { @@ -51,7 +51,7 @@ namespace Umbraco.Web.BackOffice.Authorization return Task.CompletedTask; } - private bool IsAuthorized(TreeAliasesRequirement requirement) + private bool IsAuthorized(TreeRequirement requirement) { var apps = requirement.TreeAliases.Select(x => _treeService .GetByAlias(x)) diff --git a/src/Umbraco.Web.BackOffice/Authorization/TreeAliasesRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/TreeRequirement.cs similarity index 60% rename from src/Umbraco.Web.BackOffice/Authorization/TreeAliasesRequirement.cs rename to src/Umbraco.Web.BackOffice/Authorization/TreeRequirement.cs index 44ecb8dfe3..7261cee51b 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/TreeAliasesRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/TreeRequirement.cs @@ -5,15 +5,15 @@ namespace Umbraco.Web.BackOffice.Authorization { /// - /// Authorization requirements for + /// Authorization requirements for /// - public class TreeAliasesRequirement : IAuthorizationRequirement + public class TreeRequirement : IAuthorizationRequirement { /// /// The aliases for trees that the user will need access to /// public IReadOnlyCollection TreeAliases { get; } - public TreeAliasesRequirement(params string[] aliases) => TreeAliases = aliases; + public TreeRequirement(params string[] aliases) => TreeAliases = aliases; } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizationHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs similarity index 88% rename from src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizationHandler.cs rename to src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs index f34c58411f..5a9f21df81 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizationHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Authorizes that the current user has access to the user group Id in the request /// - public class UserGroupAuthorizationHandler : AuthorizationHandler + public class UserGroupHandler : AuthorizationHandler { private readonly IHttpContextAccessor _httpContextAcessor; private readonly IUserService _userService; @@ -21,7 +21,7 @@ namespace Umbraco.Web.BackOffice.Authorization private readonly IEntityService _entityService; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - public UserGroupAuthorizationHandler(IHttpContextAccessor httpContextAcessor, + public UserGroupHandler(IHttpContextAccessor httpContextAcessor, IUserService userService, IContentService contentService, IMediaService mediaService, @@ -36,7 +36,7 @@ namespace Umbraco.Web.BackOffice.Authorization _backofficeSecurityAccessor = backofficeSecurityAccessor; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserGroupAuthorizeRequirement requirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserGroupRequirement requirement) { var isAuth = IsAuthorized(requirement); if (!isAuth.HasValue || isAuth.Value) @@ -51,7 +51,7 @@ namespace Umbraco.Web.BackOffice.Authorization return Task.CompletedTask; } - private bool? IsAuthorized(UserGroupAuthorizeRequirement requirement) + private bool? IsAuthorized(UserGroupRequirement requirement) { var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; diff --git a/src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizeRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/UserGroupRequirement.cs similarity index 52% rename from src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizeRequirement.cs rename to src/Umbraco.Web.BackOffice/Authorization/UserGroupRequirement.cs index 5a411b89fd..aae5733d96 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/UserGroupAuthorizeRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/UserGroupRequirement.cs @@ -3,11 +3,11 @@ namespace Umbraco.Web.BackOffice.Authorization { /// - /// Authorization requirement for the + /// Authorization requirement for the /// - public class UserGroupAuthorizeRequirement : IAuthorizationRequirement + public class UserGroupRequirement : IAuthorizationRequirement { - public UserGroupAuthorizeRequirement(string queryStringName = "id") + public UserGroupRequirement(string queryStringName = "id") { QueryStringName = queryStringName; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index a60a773b72..06f6d3b052 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -39,6 +39,8 @@ using Umbraco.Web.Common.Filters; using Umbraco.Web.Models.Mapping; using Microsoft.AspNetCore.Authorization; using Umbraco.Web.Common.Authorization; +using Umbraco.Web.BackOffice.Authorization; +using System.Threading.Tasks; namespace Umbraco.Web.BackOffice.Controllers { @@ -68,6 +70,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly ActionCollection _actionCollection; private readonly IMemberGroupService _memberGroupService; private readonly ISqlContext _sqlContext; + private readonly IAuthorizationService _authorizationService; private readonly Lazy> _allLangs; private readonly ILogger _logger; @@ -97,7 +100,8 @@ namespace Umbraco.Web.BackOffice.Controllers ActionCollection actionCollection, IMemberGroupService memberGroupService, ISqlContext sqlContext, - IJsonSerializer serializer) + IJsonSerializer serializer, + IAuthorizationService authorizationService) : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, serializer) { _propertyEditors = propertyEditors; @@ -119,6 +123,7 @@ namespace Umbraco.Web.BackOffice.Controllers _actionCollection = actionCollection; _memberGroupService = memberGroupService; _sqlContext = sqlContext; + _authorizationService = authorizationService; _logger = loggerFactory.CreateLogger(); _allLangs = new Lazy>(() => _localizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase)); @@ -158,16 +163,22 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Permission check is done for letter 'R' which is for which the user must have access to update /// - [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] - public ActionResult> PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) - { - if (saveModel.ContentId <= 0) return NotFound(); + public async Task>> PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) + { if (saveModel.ContentId <= 0) return NotFound(); // TODO: Should non-admins be allowed to set granular permissions? var content = _contentService.GetById(saveModel.ContentId); if (content == null) return NotFound(); + // Authorize... + var requirement = new ContentPermissionResourceRequirement(ActionRights.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, content, requirement); + if (!authorizationResult.Succeeded) + { + return Forbid(); + } + //current permissions explicitly assigned to this content item var contentPermissions = _contentService.GetPermissions(content) .ToDictionary(x => x.UserGroupId, x => x); @@ -220,7 +231,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Permission check is done for letter 'R' which is for which the user must have access to view /// - [EnsureUserPermissionForContent("contentId", 'R')] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionAdministrationById)] public ActionResult> GetDetailedPermissions(int contentId) { if (contentId <= 0) return NotFound(); @@ -336,8 +347,8 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [EnsureUserPermissionForContent("id")] + [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] [DetermineAmbiguousActionByPassingParameters] public ContentItemDisplay GetById(int id) { @@ -357,7 +368,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [EnsureUserPermissionForContent("id")] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] [DetermineAmbiguousActionByPassingParameters] public ContentItemDisplay GetById(Guid id) { @@ -378,7 +389,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [EnsureUserPermissionForContent("id")] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] [DetermineAmbiguousActionByPassingParameters] public ContentItemDisplay GetById(Udi id) { @@ -1491,8 +1502,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The EnsureUserPermissionForContent attribute will deny access to this method if the current user /// does not have Publish access to this node. /// - /// - [EnsureUserPermissionForContent("id", 'U')] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)] public IActionResult PostPublishById(int id) { var foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); @@ -1539,7 +1549,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The CanAccessContentAuthorize attribute will deny access to this method if the current user /// does not have Delete access to this node. /// - [EnsureUserPermissionForContent("id", ActionDelete.ActionLetter)] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionDeleteById)] [HttpDelete] [HttpPost] public IActionResult DeleteById(int id) @@ -1585,7 +1595,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// [HttpDelete] [HttpPost] - [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, ActionDelete.ActionLetter)] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionEmptyRecycleBin)] public IActionResult EmptyRecycleBin() { _contentService.EmptyRecycleBin(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); @@ -1598,8 +1608,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [EnsureUserPermissionForContent("sorted.ParentId", 'S')] - public IActionResult PostSort(ContentSortOrder sorted) + public async Task PostSort(ContentSortOrder sorted) { if (sorted == null) { @@ -1612,12 +1621,18 @@ namespace Umbraco.Web.BackOffice.Controllers return Ok(); } + // Authorize... + var requirement = new ContentPermissionResourceRequirement(ActionSort.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, _contentService.GetById(sorted.ParentId), requirement); + if (!authorizationResult.Succeeded) + { + return Forbid(); + } + try { - var contentService = _contentService; - // Save content with new sort order and update content xml in db accordingly - var sortResult = contentService.Sort(sorted.IdSortOrder, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + var sortResult = _contentService.Sort(sorted.IdSortOrder, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); if (!sortResult.Success) { _logger.LogWarning("Content sorting failed, this was probably caused by an event being cancelled"); @@ -1639,9 +1654,16 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [EnsureUserPermissionForContent("move.ParentId", 'M')] - public IActionResult PostMove(MoveOrCopy move) + public async Task PostMove(MoveOrCopy move) { + // Authorize... + var requirement = new ContentPermissionResourceRequirement(ActionMove.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, _contentService.GetById(move.ParentId), requirement); + if (!authorizationResult.Succeeded) + { + return Forbid(); + } + var toMove = ValidateMoveOrCopy(move); _contentService.Move(toMove, move.ParentId, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); @@ -1654,9 +1676,16 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [EnsureUserPermissionForContent("copy.ParentId", 'C')] - public IActionResult PostCopy(MoveOrCopy copy) + public async Task PostCopy(MoveOrCopy copy) { + // Authorize... + var requirement = new ContentPermissionResourceRequirement(ActionCopy.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, _contentService.GetById(copy.ParentId), requirement); + if (!authorizationResult.Succeeded) + { + return Forbid(); + } + var toCopy = ValidateMoveOrCopy(copy); var c = _contentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); @@ -1669,14 +1698,23 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// The content and variants to unpublish /// - [EnsureUserPermissionForContent("model.Id", 'Z')] [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - public ContentItemDisplay PostUnpublish(UnpublishContent model) + public async Task> PostUnpublish(UnpublishContent model) { - var foundContent = GetObjectFromRequest(() => _contentService.GetById(model.Id)); + var foundContent = _contentService.GetById(model.Id); if (foundContent == null) + { HandleContentNotFound(model.Id); + } + + // Authorize... + var requirement = new ContentPermissionResourceRequirement(ActionUnpublish.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, foundContent, requirement); + if (!authorizationResult.Succeeded) + { + return Forbid(); + } var languageCount = _allLangs.Value.Count(); if (model.Cultures.Length == 0 || model.Cultures.Length == languageCount) @@ -2267,7 +2305,7 @@ namespace Umbraco.Web.BackOffice.Controllers return display; } - [EnsureUserPermissionForContent("contentId", ActionBrowse.ActionLetter)] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] public ActionResult> GetNotificationOptions(int contentId) { var notifications = new List(); @@ -2359,7 +2397,7 @@ namespace Umbraco.Web.BackOffice.Controllers : content.Variants.FirstOrDefault(x => x.Language.IsoCode == culture); } - [EnsureUserPermissionForContent("contentId", ActionRollback.ActionLetter)] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionRollbackById)] [HttpPost] public IActionResult PostRollbackContent(int contentId, int versionId, string culture = "*") { @@ -2391,7 +2429,7 @@ namespace Umbraco.Web.BackOffice.Controllers throw HttpResponseException.CreateValidationErrorResponse(notificationModel); } - [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] [HttpGet] public IActionResult GetPublicAccess(int contentId) { @@ -2440,7 +2478,7 @@ namespace Umbraco.Web.BackOffice.Controllers } // set up public access using role based access - [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] [HttpPost] public IActionResult PostPublicAccess(int contentId, [FromQuery(Name = "groups[]")]string[] groups, [FromQuery(Name = "usernames[]")]string[] usernames, int loginPageId, int errorPageId) { @@ -2507,7 +2545,7 @@ namespace Umbraco.Web.BackOffice.Controllers : Problem(); } - [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] [HttpPost] public IActionResult RemovePublicAccess(int contentId) { diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 81674ca11c..8e3eb96660 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Security; using Umbraco.Core.Serialization; using Umbraco.Infrastructure.BackOffice; using Umbraco.Net; +using Umbraco.Web.Actions; using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Security; @@ -120,39 +121,86 @@ namespace Umbraco.Extensions // NOTE: Even though we are registering these handlers globally they will only actually execute their logic for // any auth defining a matching requirement and scheme. - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddAuthorization(options => { + // these are the query strings we will check for content ids when permission checking + var contentPermissionQueryStrings = new[] { "id", "contentId" }; + + options.AddPolicy(AuthorizationPolicies.ContentPermissionEmptyRecycleBin, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(Constants.System.RecycleBinContent, ActionDelete.ActionLetter)); + }); + + options.AddPolicy(AuthorizationPolicies.ContentPermissionAdministrationById, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter, contentPermissionQueryStrings)); + }); + + options.AddPolicy(AuthorizationPolicies.ContentPermissionProtectById, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter, contentPermissionQueryStrings)); + }); + + options.AddPolicy(AuthorizationPolicies.ContentPermissionRollbackById, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter, contentPermissionQueryStrings)); + }); + + options.AddPolicy(AuthorizationPolicies.ContentPermissionBrowseById, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionPublish.ActionLetter, contentPermissionQueryStrings)); + }); + + options.AddPolicy(AuthorizationPolicies.ContentPermissionBrowseById, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter, contentPermissionQueryStrings)); + }); + + options.AddPolicy(AuthorizationPolicies.ContentPermissionDeleteById, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter, contentPermissionQueryStrings)); + }); + options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new BackOfficeAuthorizeRequirement()); + policy.Requirements.Add(new BackOfficeRequirement()); }); options.AddPolicy(AuthorizationPolicies.BackOfficeAccessWithoutApproval, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new BackOfficeAuthorizeRequirement(false)); + policy.Requirements.Add(new BackOfficeRequirement(false)); }); options.AddPolicy(AuthorizationPolicies.AdminUserEditsRequireAdmin, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new AdminUsersAuthorizeRequirement()); - policy.Requirements.Add(new AdminUsersAuthorizeRequirement("userIds")); + policy.Requirements.Add(new AdminUsersRequirement()); + policy.Requirements.Add(new AdminUsersRequirement("userIds")); }); options.AddPolicy(AuthorizationPolicies.UserBelongsToUserGroupInRequest, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new UserGroupAuthorizeRequirement()); - policy.Requirements.Add(new UserGroupAuthorizeRequirement("userGroupIds")); + policy.Requirements.Add(new UserGroupRequirement()); + policy.Requirements.Add(new UserGroupRequirement("userGroupIds")); }); options.AddPolicy(AuthorizationPolicies.DenyLocalLoginIfConfigured, policy => @@ -164,50 +212,50 @@ namespace Umbraco.Extensions options.AddPolicy(AuthorizationPolicies.SectionAccessContent, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Content)); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Content)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Content, Constants.Applications.Media)); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Content, Constants.Applications.Media)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessUsers, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Users)); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Users)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessForTinyMce, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement( + policy.Requirements.Add(new SectionRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessMedia, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Media)); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Media)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessMembers, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Members)); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Members)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessPackages, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Packages)); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Packages)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessSettings, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement(Constants.Applications.Settings)); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Settings)); }); //We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered @@ -215,21 +263,21 @@ namespace Umbraco.Extensions options.AddPolicy(AuthorizationPolicies.SectionAccessForContentTree, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement( + policy.Requirements.Add(new SectionRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessForMediaTree, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement( + policy.Requirements.Add(new SectionRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessForMemberTree, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement( + policy.Requirements.Add(new SectionRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); }); @@ -237,7 +285,7 @@ namespace Umbraco.Extensions options.AddPolicy(AuthorizationPolicies.SectionAccessForDataTypeReading, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new SectionAliasesRequirement( + policy.Requirements.Add(new SectionRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings, Constants.Applications.Packages)); }); @@ -245,139 +293,139 @@ namespace Umbraco.Extensions options.AddPolicy(AuthorizationPolicies.TreeAccessDocuments, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Content)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Content)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessUsers, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Users)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Users)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.PartialViews)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.PartialViews)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViewMacros, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.PartialViewMacros)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.PartialViewMacros)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessPackages, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Packages)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Packages)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessLogs, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.LogViewer)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.LogViewer)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DataTypes)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.DataTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessTemplates, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Templates)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Templates)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberTypes)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.RelationTypes)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.RelationTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DocumentTypes)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.DocumentTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberGroups)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberGroups)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MediaTypes)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MediaTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMacros, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Macros)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Macros)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessLanguages, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Languages)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Languages)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDictionary, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary, Constants.Trees.Dictionary)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary, Constants.Trees.Dictionary)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.Dictionary, Constants.Trees.Templates)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary, Constants.Trees.Templates)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DocumentTypes, Constants.Trees.Content)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.DocumentTypes, Constants.Trees.Content)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MediaTypes, Constants.Trees.Media)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MediaTypes, Constants.Trees.Media)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMembersOrMemberTypes, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.MemberTypes, Constants.Trees.Members)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberTypes, Constants.Trees.Members)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessAnySchemaTypes, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessAnyContentOrTypes, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new TreeAliasesRequirement( + policy.Requirements.Add(new TreeRequirement( Constants.Trees.DocumentTypes, Constants.Trees.Content, Constants.Trees.MediaTypes, Constants.Trees.Media, Constants.Trees.MemberTypes, Constants.Trees.Members)); diff --git a/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForContentAttribute.cs deleted file mode 100644 index 6e0095b4b4..0000000000 --- a/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForContentAttribute.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Net; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Web.Actions; -using Umbraco.Web.Common.Exceptions; -using Umbraco.Web.Security; - -namespace Umbraco.Web.BackOffice.Filters -{ - /// - /// Auth filter to check if the current user has access to the content item (by id). - /// - /// - /// This first checks if the user can access this based on their start node, and then checks node permissions - /// By default the permission that is checked is browse but this can be specified in the ctor. - /// NOTE: This cannot be an auth filter because that happens too soon and we don't have access to the action params. - /// - public sealed class EnsureUserPermissionForContentAttribute : TypeFilterAttribute - { - - /// - /// This constructor will only be able to test the start node access - /// - public EnsureUserPermissionForContentAttribute(int nodeId) - : base(typeof(EnsureUserPermissionForContentFilter)) - { - Arguments = new object[] - { - nodeId - }; - } - - - public EnsureUserPermissionForContentAttribute(int nodeId, char permissionToCheck) - : base(typeof(EnsureUserPermissionForContentFilter)) - { - Arguments = new object[] - { - nodeId, permissionToCheck - }; - } - - public EnsureUserPermissionForContentAttribute(string paramName) - : base(typeof(EnsureUserPermissionForContentFilter)) - { - if (paramName == null) throw new ArgumentNullException(nameof(paramName)); - if (string.IsNullOrEmpty(paramName)) - throw new ArgumentException("Value can't be empty.", nameof(paramName)); - - Arguments = new object[] - { - paramName, ActionBrowse.ActionLetter - }; - } - - - public EnsureUserPermissionForContentAttribute(string paramName, char permissionToCheck) - : base(typeof(EnsureUserPermissionForContentFilter)) - { - if (paramName == null) throw new ArgumentNullException(nameof(paramName)); - if (string.IsNullOrEmpty(paramName)) - throw new ArgumentException("Value can't be empty.", nameof(paramName)); - - Arguments = new object[] - { - paramName, permissionToCheck - }; - } - - private sealed class EnsureUserPermissionForContentFilter : IActionFilter - { - private readonly int? _nodeId; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - private readonly IEntityService _entityService; - private readonly IUserService _userService; - private readonly IContentService _contentService; - private readonly string _paramName; - private readonly char? _permissionToCheck; - - public EnsureUserPermissionForContentFilter( - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IEntityService entityService, - IUserService userService, - IContentService contentService, - string paramName) - :this(backofficeSecurityAccessor, entityService, userService, contentService, null, paramName, ActionBrowse.ActionLetter) - { - - } - - public EnsureUserPermissionForContentFilter( - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IEntityService entityService, - IUserService userService, - IContentService contentService, - int nodeId, - char permissionToCheck) - :this(backofficeSecurityAccessor, entityService, userService, contentService, nodeId, null, permissionToCheck) - { - - } - - public EnsureUserPermissionForContentFilter( - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IEntityService entityService, - IUserService userService, - IContentService contentService, - int nodeId) - :this(backofficeSecurityAccessor, entityService, userService, contentService, nodeId, null, null) - { - - } - public EnsureUserPermissionForContentFilter( - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IEntityService entityService, - IUserService userService, - IContentService contentService, - string paramName, char permissionToCheck) - :this(backofficeSecurityAccessor, entityService, userService, contentService, null, paramName, permissionToCheck) - { - - } - - - private EnsureUserPermissionForContentFilter( - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IEntityService entityService, - IUserService userService, - IContentService contentService, - int? nodeId, string paramName, char? permissionToCheck) - { - _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); - - _paramName = paramName; - if (permissionToCheck.HasValue) - { - _permissionToCheck = permissionToCheck.Value; - } - - - if (nodeId.HasValue) - { - _nodeId = nodeId.Value; - } - } - - - - public void OnActionExecuting(ActionExecutingContext context) - { - if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser == null) - { - //not logged in - throw new HttpResponseException(HttpStatusCode.Unauthorized); - } - - int nodeId; - if (_nodeId.HasValue == false) - { - var parts = _paramName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - - if (context.ActionArguments[parts[0]] == null) - { - throw new InvalidOperationException("No argument found for the current action with the name: " + - _paramName); - } - - if (parts.Length == 1) - { - var argument = context.ActionArguments[parts[0]].ToString(); - // if the argument is an int, it will parse and can be assigned to nodeId - // if might be a udi, so check that next - // otherwise treat it as a guid - unlikely we ever get here - if (int.TryParse(argument, out int parsedId)) - { - nodeId = parsedId; - } - else if (UdiParser.TryParse(argument, true, out var udi)) - { - nodeId = _entityService.GetId(udi).Result; - } - else - { - Guid.TryParse(argument, out Guid key); - nodeId = _entityService.GetId(key, UmbracoObjectTypes.Document).Result; - } - } - else - { - //now we need to see if we can get the property of whatever object it is - var pType = context.ActionArguments[parts[0]].GetType(); - var prop = pType.GetProperty(parts[1]); - if (prop == null) - { - throw new InvalidOperationException( - "No argument found for the current action with the name: " + _paramName); - } - - nodeId = (int) prop.GetValue(context.ActionArguments[parts[0]]); - } - } - else - { - nodeId = _nodeId.Value; - } - - var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, - _userService, - _contentService, - _entityService, - out var contentItem, - _permissionToCheck.HasValue ? new[] { _permissionToCheck.Value } : null); - - if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound) - { - context.Result = new NotFoundResult(); - return; - } - - if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) - { - context.Result = new ForbidResult(); - return; - } - - - if (contentItem != null) - { - //store the content item in request cache so it can be resolved in the controller without re-looking it up - context.HttpContext.Items[typeof(IContent).ToString()] = contentItem; - } - - } - - public void OnActionExecuted(ActionExecutedContext context) - { - - } - - - } - } -} diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index 9f0a6ddd1f..644ee4dd71 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -13,6 +13,16 @@ public const string AdminUserEditsRequireAdmin = nameof(AdminUserEditsRequireAdmin); public const string DenyLocalLoginIfConfigured = nameof(DenyLocalLoginIfConfigured); + // Content permission access + + public const string ContentPermissionEmptyRecycleBin = nameof(ContentPermissionEmptyRecycleBin); + public const string ContentPermissionAdministrationById = nameof(ContentPermissionAdministrationById); + public const string ContentPermissionPublishById = nameof(ContentPermissionPublishById); + public const string ContentPermissionRollbackById = nameof(ContentPermissionRollbackById); + public const string ContentPermissionProtectById = nameof(ContentPermissionProtectById); + public const string ContentPermissionBrowseById = nameof(ContentPermissionBrowseById); + public const string ContentPermissionDeleteById = nameof(ContentPermissionDeleteById); + // Single section access public const string SectionAccessContent = nameof(SectionAccessContent); diff --git a/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs index d8b9ab7ff4..3f2fc0b6bb 100644 --- a/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs +++ b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; +using System; using System.Threading.Tasks; using Umbraco.Web.Features; @@ -34,14 +35,14 @@ namespace Umbraco.Web.BackOffice.Authorization private bool? IsAllowed(AuthorizationHandlerContext context) { - if (context.Resource is Endpoint endpoint) + if (!(context.Resource is Endpoint endpoint)) { - var actionDescriptor = endpoint.Metadata.GetMetadata(); - var controllerType = actionDescriptor.ControllerTypeInfo.AsType(); - return _umbracoFeatures.IsControllerEnabled(controllerType); + throw new InvalidOperationException("This authorization handler can only be applied to controllers routed with endpoint routing"); } - return null; + var actionDescriptor = endpoint.Metadata.GetMetadata(); + var controllerType = actionDescriptor.ControllerTypeInfo.AsType(); + return _umbracoFeatures.IsControllerEnabled(controllerType); } } } From 4dbfe5867bfbf7164d31c9d0d0e32d2b4aaf15ab Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Nov 2020 00:37:26 +1100 Subject: [PATCH 13/25] migrates the custom content persmission helper and media permissions helper checks to authz policies and cleans up that code/class/namespaces --- src/Umbraco.Core/Models/UserExtensions.cs | 16 +- ...issionsHelper.cs => ContentPermissions.cs} | 131 ++++--- src/Umbraco.Core/Security/MediaPermissions.cs | 75 ++++ .../Editors/UserEditorAuthorizationHelper.cs | 4 +- .../Security/ContentPermissionsTests.cs} | 131 +++---- .../Security/MediaPermissionsTests.cs} | 46 +-- .../ContentPermissionPublishBranchHandler.cs | 70 ++++ .../ContentPermissionQueryStringHandler.cs | 61 +--- .../ContentPermissionResourceHandler.cs | 43 +++ .../ContentPermissionResourceRequirement.cs | 19 +- ...tentPermissionsPublishBranchRequirement.cs | 17 + .../MediaPermissionQueryStringHandler.cs | 94 +++++ .../MediaPermissionResourceHandler.cs | 54 +++ .../MediaPermissionResourceRequirement.cs | 21 ++ .../MediaPermissionsQueryStringRequirement.cs | 14 + .../Controllers/ContentController.cs | 45 +-- .../Controllers/MediaController.cs | 137 +++----- .../BackOfficeServiceCollectionExtensions.cs | 12 + .../Filters/ContentSaveValidationAttribute.cs | 75 ++-- .../EnsureUserPermissionForMediaAttribute.cs | 324 +++++++++--------- .../FilterAllowedOutgoingMediaAttribute.cs | 2 +- .../MediaItemSaveValidationAttribute.cs | 49 +-- .../Trees/ContentTreeControllerBase.cs | 2 +- .../Authorization/AuthorizationPolicies.cs | 2 + .../Runtime/AspNetCoreComposer.cs | 3 + 25 files changed, 882 insertions(+), 565 deletions(-) rename src/Umbraco.Core/Security/{ContentPermissionsHelper.cs => ContentPermissions.cs} (69%) create mode 100644 src/Umbraco.Core/Security/MediaPermissions.cs rename src/Umbraco.Tests.UnitTests/{Umbraco.Web.BackOffice/Controllers/ContentControllerUnitTests.cs => Umbraco.Core/Security/ContentPermissionsTests.cs} (70%) rename src/Umbraco.Tests.UnitTests/{Umbraco.Web.BackOffice/Controllers/MediaControllerUnitTests.cs => Umbraco.Core/Security/MediaPermissionsTests.cs} (73%) create mode 100644 src/Umbraco.Web.BackOffice/Authorization/ContentPermissionPublishBranchHandler.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceHandler.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchRequirement.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/MediaPermissionQueryStringHandler.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceHandler.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceRequirement.cs create mode 100644 src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringRequirement.cs diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 295b764834..6a3ea16ff7 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -92,46 +92,46 @@ namespace Umbraco.Core.Models public static bool HasContentRootAccess(this IUser user, IEntityService entityService) { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } public static bool HasContentBinAccess(this IUser user, IEntityService entityService) { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissions.HasPathAccess(Constants.System.RecycleBinContentString, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } public static bool HasMediaRootAccess(this IUser user, IEntityService entityService) { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissions.HasPathAccess(Constants.System.RootString, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } public static bool HasMediaBinAccess(this IUser user, IEntityService entityService) { - return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissions.HasPathAccess(Constants.System.RecycleBinMediaString, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } public static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService) { if (content == null) throw new ArgumentNullException(nameof(content)); - return ContentPermissionsHelper.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissions.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } public static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService) { if (media == null) throw new ArgumentNullException(nameof(media)); - return ContentPermissionsHelper.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissions.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } public static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) { if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); + return ContentPermissions.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent); } public static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService) { if (entity == null) throw new ArgumentNullException(nameof(entity)); - return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); + return ContentPermissions.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } /// diff --git a/src/Umbraco.Core/Security/ContentPermissionsHelper.cs b/src/Umbraco.Core/Security/ContentPermissions.cs similarity index 69% rename from src/Umbraco.Core/Security/ContentPermissionsHelper.cs rename to src/Umbraco.Core/Security/ContentPermissions.cs index 7f0a5c3c24..b598897133 100644 --- a/src/Umbraco.Core/Security/ContentPermissionsHelper.cs +++ b/src/Umbraco.Core/Security/ContentPermissions.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; @@ -11,8 +9,16 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Security { - public class ContentPermissionsHelper + + /// + /// Checks user access to content + /// + public class ContentPermissions { + private readonly IUserService _userService; + private readonly IContentService _contentService; + private readonly IEntityService _entityService; + public enum ContentAccess { Granted, @@ -20,56 +26,68 @@ namespace Umbraco.Core.Security NotFound } - public static ContentAccess CheckPermissions( + public ContentPermissions( + IUserService userService, + IContentService contentService, + IEntityService entityService) + { + _userService = userService; + _contentService = contentService; + _entityService = entityService; + } + + public ContentAccess CheckPermissions( IContent content, IUser user, - IUserService userService, - IEntityService entityService, - params char[] permissionsToCheck) + char permissionToCheck) => CheckPermissions(content, user, new[] { permissionToCheck }); + + public ContentAccess CheckPermissions( + IContent content, + IUser user, + IReadOnlyList permissionsToCheck) { - if (user == null) throw new ArgumentNullException("user"); - if (userService == null) throw new ArgumentNullException("userService"); - if (entityService == null) throw new ArgumentNullException("entityService"); + if (user == null) throw new ArgumentNullException(nameof(user)); if (content == null) return ContentAccess.NotFound; - var hasPathAccess = user.HasPathAccess(content, entityService); + var hasPathAccess = user.HasPathAccess(content, _entityService); if (hasPathAccess == false) return ContentAccess.Denied; - if (permissionsToCheck == null || permissionsToCheck.Length == 0) + if (permissionsToCheck == null || permissionsToCheck.Count == 0) return ContentAccess.Granted; //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(content.Path, user, userService, permissionsToCheck) + return CheckPermissionsPath(content.Path, user) ? ContentAccess.Granted : ContentAccess.Denied; } - public static ContentAccess CheckPermissions( + public ContentAccess CheckPermissions( IUmbracoEntity entity, IUser user, - IUserService userService, - IEntityService entityService, - params char[] permissionsToCheck) + char permissionToCheck) => CheckPermissions(entity, user, new[] { permissionToCheck }); + + public ContentAccess CheckPermissions( + IUmbracoEntity entity, + IUser user, + IReadOnlyList permissionsToCheck) { - if (user == null) throw new ArgumentNullException("user"); - if (userService == null) throw new ArgumentNullException("userService"); - if (entityService == null) throw new ArgumentNullException("entityService"); + if (user == null) throw new ArgumentNullException(nameof(user)); if (entity == null) return ContentAccess.NotFound; - var hasPathAccess = user.HasContentPathAccess(entity, entityService); + var hasPathAccess = user.HasContentPathAccess(entity, _entityService); if (hasPathAccess == false) return ContentAccess.Denied; - if (permissionsToCheck == null || permissionsToCheck.Length == 0) + if (permissionsToCheck == null || permissionsToCheck.Count == 0) return ContentAccess.Granted; //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(entity.Path, user, userService, permissionsToCheck) + return CheckPermissionsPath(entity.Path, user) ? ContentAccess.Granted : ContentAccess.Denied; } @@ -84,41 +102,42 @@ namespace Umbraco.Core.Security /// The item resolved if one was found for the id /// /// - public static ContentAccess CheckPermissions( + public ContentAccess CheckPermissions( int nodeId, IUser user, - IUserService userService, - IEntityService entityService, out IUmbracoEntity entity, - params char[] permissionsToCheck) + IReadOnlyList permissionsToCheck = null) { - if (user == null) throw new ArgumentNullException("user"); - if (userService == null) throw new ArgumentNullException("userService"); - if (entityService == null) throw new ArgumentNullException("entityService"); + if (user == null) throw new ArgumentNullException(nameof(user)); + + if (permissionsToCheck == null) + { + permissionsToCheck = Array.Empty(); + } bool? hasPathAccess = null; entity = null; if (nodeId == Constants.System.Root) - hasPathAccess = user.HasContentRootAccess(entityService); + hasPathAccess = user.HasContentRootAccess(_entityService); else if (nodeId == Constants.System.RecycleBinContent) - hasPathAccess = user.HasContentBinAccess(entityService); + hasPathAccess = user.HasContentBinAccess(_entityService); if (hasPathAccess.HasValue) return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; - entity = entityService.Get(nodeId, UmbracoObjectTypes.Document); + entity = _entityService.Get(nodeId, UmbracoObjectTypes.Document); if (entity == null) return ContentAccess.NotFound; - hasPathAccess = user.HasContentPathAccess(entity, entityService); + hasPathAccess = user.HasContentPathAccess(entity, _entityService); if (hasPathAccess == false) return ContentAccess.Denied; - if (permissionsToCheck == null || permissionsToCheck.Length == 0) + if (permissionsToCheck == null || permissionsToCheck.Count == 0) return ContentAccess.Granted; //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(entity.Path, user, userService, permissionsToCheck) + return CheckPermissionsPath(entity.Path, user, permissionsToCheck) ? ContentAccess.Granted : ContentAccess.Denied; } @@ -134,52 +153,56 @@ namespace Umbraco.Core.Security /// The item resolved if one was found for the id /// /// - public static ContentAccess CheckPermissions( + public ContentAccess CheckPermissions( int nodeId, - IUser user, - IUserService userService, - IContentService contentService, - IEntityService entityService, + IUser user, out IContent contentItem, - params char[] permissionsToCheck) + IReadOnlyList permissionsToCheck = null) { - if (user == null) throw new ArgumentNullException("user"); - if (userService == null) throw new ArgumentNullException("userService"); - if (contentService == null) throw new ArgumentNullException("contentService"); - if (entityService == null) throw new ArgumentNullException("entityService"); + if (user == null) throw new ArgumentNullException(nameof(user)); + + if (permissionsToCheck == null) + { + permissionsToCheck = Array.Empty(); + } bool? hasPathAccess = null; contentItem = null; if (nodeId == Constants.System.Root) - hasPathAccess = user.HasContentRootAccess(entityService); + hasPathAccess = user.HasContentRootAccess(_entityService); else if (nodeId == Constants.System.RecycleBinContent) - hasPathAccess = user.HasContentBinAccess(entityService); + hasPathAccess = user.HasContentBinAccess(_entityService); if (hasPathAccess.HasValue) return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied; - contentItem = contentService.GetById(nodeId); + contentItem = _contentService.GetById(nodeId); if (contentItem == null) return ContentAccess.NotFound; - hasPathAccess = user.HasPathAccess(contentItem, entityService); + hasPathAccess = user.HasPathAccess(contentItem, _entityService); if (hasPathAccess == false) return ContentAccess.Denied; - if (permissionsToCheck == null || permissionsToCheck.Length == 0) + if (permissionsToCheck == null || permissionsToCheck.Count == 0) return ContentAccess.Granted; //get the implicit/inherited permissions for the user for this path - return CheckPermissionsPath(contentItem.Path, user, userService, permissionsToCheck) + return CheckPermissionsPath(contentItem.Path, user, permissionsToCheck) ? ContentAccess.Granted : ContentAccess.Denied; } - private static bool CheckPermissionsPath(string path, IUser user, IUserService userService, params char[] permissionsToCheck) + private bool CheckPermissionsPath(string path, IUser user, IReadOnlyList permissionsToCheck = null) { + if (permissionsToCheck == null) + { + permissionsToCheck = Array.Empty(); + } + //get the implicit/inherited permissions for the user for this path, //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) - var permission = userService.GetPermissionsForPath(user, path); + var permission = _userService.GetPermissionsForPath(user, path); var allowed = true; foreach (var p in permissionsToCheck) diff --git a/src/Umbraco.Core/Security/MediaPermissions.cs b/src/Umbraco.Core/Security/MediaPermissions.cs new file mode 100644 index 0000000000..65da95ec02 --- /dev/null +++ b/src/Umbraco.Core/Security/MediaPermissions.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Security +{ + /// + /// Checks user access to media + /// + public class MediaPermissions + { + private readonly IMediaService _mediaService; + private readonly IEntityService _entityService; + + public enum MediaAccess + { + Granted, + Denied, + NotFound + } + + public MediaPermissions(IMediaService mediaService, IEntityService entityService) + { + _mediaService = mediaService; + _entityService = entityService; + } + + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// + /// + /// + /// The content to lookup, if the contentItem is not specified + /// + public MediaAccess CheckPermissions(IUser user, int nodeId, out IMedia media) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + media = null; + + if (nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) + { + media = _mediaService.GetById(nodeId); + } + + if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) + { + return MediaAccess.NotFound; + } + + var hasPathAccess = (nodeId == Constants.System.Root) + ? user.HasMediaRootAccess(_entityService) + : (nodeId == Constants.System.RecycleBinMedia) + ? user.HasMediaBinAccess(_entityService) + : user.HasPathAccess(media, _entityService); + + return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; + } + + public MediaAccess CheckPermissions(IMedia media, IUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + if (media == null) return MediaAccess.NotFound; + + var hasPathAccess = user.HasPathAccess(media, _entityService); + + return hasPathAccess ? MediaAccess.Granted : MediaAccess.Denied; + } + } +} diff --git a/src/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelper.cs b/src/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelper.cs index d19f4d954c..d8a5f675b2 100644 --- a/src/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelper.cs +++ b/src/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelper.cs @@ -112,7 +112,7 @@ namespace Umbraco.Web.Editors { if (contentId == Constants.System.Root) { - var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService), Constants.System.RecycleBinContent); + var hasAccess = ContentPermissions.HasPathAccess("-1", currentUser.CalculateContentStartNodeIds(_entityService), Constants.System.RecycleBinContent); if (hasAccess == false) return Attempt.Fail("The current user does not have access to the content root"); } @@ -133,7 +133,7 @@ namespace Umbraco.Web.Editors { if (mediaId == Constants.System.Root) { - var hasAccess = ContentPermissionsHelper.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService), Constants.System.RecycleBinMedia); + var hasAccess = ContentPermissions.HasPathAccess("-1", currentUser.CalculateMediaStartNodeIds(_entityService), Constants.System.RecycleBinMedia); if (hasAccess == false) return Attempt.Fail("The current user does not have access to the media root"); } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/ContentControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/ContentPermissionsTests.cs similarity index 70% rename from src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/ContentControllerUnitTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/ContentPermissionsTests.cs index 3df5f76da5..8a7b26c6cf 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/ContentControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/ContentPermissionsTests.cs @@ -8,10 +8,10 @@ using Umbraco.Core.Services; using Umbraco.Tests.Common.Builders; using Umbraco.Tests.Common.Builders.Extensions; -namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers +namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security { [TestFixture] - public class ContentControllerUnitTests + public class ContentPermissionsTests { [Test] public void Access_Allowed_By_Path() @@ -28,12 +28,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var entityService = entityServiceMock.Object; var userServiceMock = new Mock(); var userService = userServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent); + var result = contentPermissions.CheckPermissions(1234, user, out IContent foundContent); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Granted, result); } [Test] @@ -54,12 +55,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var userService = userServiceMock.Object; var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); + var result = contentPermissions.CheckPermissions(1234, user, out IContent foundContent, new[] { 'F' }); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.NotFound, result); + Assert.AreEqual(ContentPermissions.ContentAccess.NotFound, result); } [Test] @@ -82,12 +84,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 9876 && entity.Path == "-1,9876") }); var entityService = entityServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); + var result = contentPermissions.CheckPermissions(1234, user, out IContent foundContent, new[] { 'F' }); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Denied, result); } [Test] @@ -111,12 +114,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var userService = userServiceMock.Object; var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); + var result = contentPermissions.CheckPermissions(1234, user, out IContent foundContent, new[] { 'F' }); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Denied, result); } [Test] @@ -140,12 +144,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var userService = userServiceMock.Object; var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(1234, user, userService, contentService, entityService, out var foundContent, new[] { 'F' }); + var result = contentPermissions.CheckPermissions(1234, user, out IContent foundContent, new[] { 'F' }); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Granted, result); } [Test] @@ -159,12 +164,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var userService = userServiceMock.Object; var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent); + var result = contentPermissions.CheckPermissions(-1, user, out IContent _); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Granted, result); } [Test] @@ -178,12 +184,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var userService = userServiceMock.Object; var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent); + var result = contentPermissions.CheckPermissions(-20, user, out IContent _); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Granted, result); } [Test] @@ -199,12 +206,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); var entityService = entityServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent); + var result = contentPermissions.CheckPermissions(-20, user, out IContent foundContent); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Denied, result); } [Test] @@ -221,12 +229,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); var entityService = entityServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent); + var result = contentPermissions.CheckPermissions(-1, user, out IContent foundContent); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Denied, result); } [Test] @@ -247,12 +256,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var userService = userServiceMock.Object; var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent, new[] { 'A' }); + var result = contentPermissions.CheckPermissions(-1, user, out IContent foundContent, new[] { 'A' }); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Granted, result); } [Test] @@ -273,12 +283,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var entityService = entityServiceMock.Object; var contentServiceMock = new Mock(); var contentService = contentServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(-1, user, userService, contentService, entityService, out var foundContent, new[] { 'B' }); + var result = contentPermissions.CheckPermissions(-1, user, out IContent foundContent, new[] { 'B' }); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Denied, result); } [Test] @@ -300,12 +311,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var entityService = entityServiceMock.Object; var contentServiceMock = new Mock(); var contentService = contentServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent, new[] { 'A' }); + var result = contentPermissions.CheckPermissions(-20, user, out IContent foundContent, new[] { 'A' }); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Granted, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Granted, result); } [Test] @@ -326,12 +338,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var entityService = entityServiceMock.Object; var contentServiceMock = new Mock(); var contentService = contentServiceMock.Object; + var contentPermissions = new ContentPermissions(userService, contentService, entityService); //act - var result = ContentPermissionsHelper.CheckPermissions(-20, user, userService, contentService, entityService, out var foundContent, new[] { 'B' }); + var result = contentPermissions.CheckPermissions(-20, user, out IContent foundContent, new[] { 'B' }); //assert - Assert.AreEqual(ContentPermissionsHelper.ContentAccess.Denied, result); + Assert.AreEqual(ContentPermissions.ContentAccess.Denied, result); } private IUser CreateUser(int id = 0, int? startContentId = null, bool withUserGroup = true) @@ -340,77 +353,15 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers .WithId(id) .WithStartContentIds(startContentId.HasValue ? new[] { startContentId.Value } : new int[0]); if (withUserGroup) - { builder = builder .AddUserGroup() .WithId(1) .WithName("admin") .WithAlias("admin") .Done(); - } return builder.Build(); } } - //NOTE: The below self hosted stuff does work so need to get some tests written. Some are not possible atm because - // of the legacy SQL calls like checking permissions. - - //[TestFixture] - //public class ContentControllerHostedTests : BaseRoutingTest - //{ - - // protected override DatabaseBehavior DatabaseTestBehavior - // { - // get { return DatabaseBehavior.NoDatabasePerFixture; } - // } - - // public override void TearDown() - // { - // base.TearDown(); - // UmbracoAuthorizeAttribute.Enable = true; - // UmbracoApplicationAuthorizeAttribute.Enable = true; - // } - - // /// - // /// Tests to ensure that the response filter works so that any items the user - // /// doesn't have access to are removed - // /// - // [Test] - // public async void Get_By_Ids_Response_Filtered() - // { - // UmbracoAuthorizeAttribute.Enable = false; - // UmbracoApplicationAuthorizeAttribute.Enable = false; - - // var baseUrl = string.Format("http://{0}:9876", Environment.MachineName); - // var url = baseUrl + "/api/Content/GetByIds?ids=1&ids=2"; - - // var routingCtx = GetRoutingContext(url, 1234, null, true); - - // var config = new HttpSelfHostConfiguration(baseUrl); - // using (var server = new HttpSelfHostServer(config)) - // { - // var route = config.Routes.MapHttpRoute("test", "api/Content/GetByIds", - // new - // { - // controller = "Content", - // action = "GetByIds", - // id = RouteParameter.Optional - // }); - // route.DataTokens["Namespaces"] = new string[] { "Umbraco.Web.Editors" }; - - // var client = new HttpClient(server); - - // var request = new HttpRequestMessage - // { - // RequestUri = new Uri(url), - // Method = HttpMethod.Get - // }; - - // var result = await client.SendAsync(request); - // } - - // } - - //} } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MediaControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/MediaPermissionsTests.cs similarity index 73% rename from src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MediaControllerUnitTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/MediaPermissionsTests.cs index 2f692ade85..c9b0324f06 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MediaControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Security/MediaPermissionsTests.cs @@ -1,19 +1,17 @@ -using System.Collections.Generic; -using Moq; +using Moq; using NUnit.Framework; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Tests.Common.Builders; using Umbraco.Tests.Common.Builders.Extensions; -using Umbraco.Web.BackOffice.Controllers; -using Umbraco.Web.Common.Exceptions; -namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers +namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security { [TestFixture] - public class MediaControllerUnitTests + public class MediaPermissionsTests { [Test] public void Access_Allowed_By_Path() @@ -28,16 +26,17 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var mediaService = mediaServiceMock.Object; var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; + var mediaPermissions = new MediaPermissions(mediaService, entityService); //act - var result = MediaController.CheckPermissions(new Dictionary(), user, mediaService, entityService, 1234); + var result = mediaPermissions.CheckPermissions(user, 1234, out _); //assert - Assert.IsTrue(result); + Assert.AreEqual(MediaPermissions.MediaAccess.Granted, result); } [Test] - public void Throws_Exception_When_No_Media_Found() + public void Returns_Not_Found_When_No_Media_Found() { //arrange var user = CreateUser(id: 9); @@ -49,9 +48,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var mediaService = mediaServiceMock.Object; var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; + var mediaPermissions = new MediaPermissions(mediaService, entityService); //act/assert - Assert.Throws(() => MediaController.CheckPermissions(new Dictionary(), user, mediaService, entityService, 1234)); + var result = mediaPermissions.CheckPermissions(user, 1234, out _); + Assert.AreEqual(MediaPermissions.MediaAccess.NotFound, result); } [Test] @@ -69,12 +70,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 9876 && entity.Path == "-1,9876") }); var entityService = entityServiceMock.Object; + var mediaPermissions = new MediaPermissions(mediaService, entityService); //act - var result = MediaController.CheckPermissions(new Dictionary(), user, mediaService, entityService, 1234); + var result = mediaPermissions.CheckPermissions(user, 1234, out _); //assert - Assert.IsFalse(result); + Assert.AreEqual(MediaPermissions.MediaAccess.Denied, result); } [Test] @@ -86,12 +88,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var mediaService = mediaServiceMock.Object; var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; + var mediaPermissions = new MediaPermissions(mediaService, entityService); //act - var result = MediaController.CheckPermissions(new Dictionary(), user, mediaService, entityService, -1); + var result = mediaPermissions.CheckPermissions(user, -1, out _); //assert - Assert.IsTrue(result); + Assert.AreEqual(MediaPermissions.MediaAccess.Granted, result); } [Test] @@ -105,12 +108,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); var entityService = entityServiceMock.Object; + var mediaPermissions = new MediaPermissions(mediaService, entityService); //act - var result = MediaController.CheckPermissions(new Dictionary(), user, mediaService, entityService, -1); + var result = mediaPermissions.CheckPermissions(user, -1, out _); //assert - Assert.IsFalse(result); + Assert.AreEqual(MediaPermissions.MediaAccess.Denied, result); } [Test] @@ -122,12 +126,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers var mediaService = mediaServiceMock.Object; var entityServiceMock = new Mock(); var entityService = entityServiceMock.Object; + var mediaPermissions = new MediaPermissions(mediaService, entityService); //act - var result = MediaController.CheckPermissions(new Dictionary(), user, mediaService, entityService, -21); + var result = mediaPermissions.CheckPermissions(user, -21, out _); //assert - Assert.IsTrue(result); + Assert.AreEqual(MediaPermissions.MediaAccess.Granted, result); } [Test] @@ -141,12 +146,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers entityServiceMock.Setup(x => x.GetAllPaths(It.IsAny(), It.IsAny())) .Returns(new[] { Mock.Of(entity => entity.Id == 1234 && entity.Path == "-1,1234") }); var entityService = entityServiceMock.Object; + var mediaPermissions = new MediaPermissions(mediaService, entityService); //act - var result = MediaController.CheckPermissions(new Dictionary(), user, mediaService, entityService, -21); + var result = mediaPermissions.CheckPermissions(user, -21, out _); //assert - Assert.IsFalse(result); + Assert.AreEqual(MediaPermissions.MediaAccess.Denied, result); } private IUser CreateUser(int id = 0, int? startMediaId = null) diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionPublishBranchHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionPublishBranchHandler.cs new file mode 100644 index 0000000000..f172bfdea0 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionPublishBranchHandler.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Authorization; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Security; +using Umbraco.Core.Services; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// The user must have access to all descendant nodes of the content item in order to continue + /// + public class ContentPermissionPublishBranchHandler : AuthorizationHandler + { + private readonly IEntityService _entityService; + private readonly ContentPermissions _contentPermissions; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public ContentPermissionPublishBranchHandler( + IEntityService entityService, + ContentPermissions contentPermissions, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _entityService = entityService; + _contentPermissions = contentPermissions; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionsPublishBranchRequirement requirement, IContent resource) + { + var denied = new List(); + var page = 0; + const int pageSize = 500; + var total = long.MaxValue; + while (page * pageSize < total) + { + var descendants = _entityService.GetPagedDescendants(resource.Id, UmbracoObjectTypes.Document, page++, pageSize, out total, + //order by shallowest to deepest, this allows us to check permissions from top to bottom so we can exit + //early if a permission higher up fails + ordering: Ordering.By("path", Direction.Ascending)); + + foreach (var c in descendants) + { + //if this item's path has already been denied or if the user doesn't have access to it, add to the deny list + if (denied.Any(x => c.Path.StartsWith($"{x.Path},")) + || (_contentPermissions.CheckPermissions(c, + _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + requirement.Permission) == ContentPermissions.ContentAccess.Denied)) + { + denied.Add(c); + } + } + } + + if (denied.Count == 0) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionQueryStringHandler.cs index b6e99d9320..fa10d5477b 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionQueryStringHandler.cs @@ -11,45 +11,6 @@ using Umbraco.Core.Services; namespace Umbraco.Web.BackOffice.Authorization { - /// - /// Used to authorize if the user has the correct permission access to the content for the specified - /// - public class ContentPermissionResourceHandler : AuthorizationHandler - { - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - private readonly IEntityService _entityService; - private readonly IUserService _userService; - - public ContentPermissionResourceHandler( - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IEntityService entityService, - IUserService userService) - { - _backofficeSecurityAccessor = backofficeSecurityAccessor; - _entityService = entityService; - _userService = userService; - } - - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionResourceRequirement requirement, IContent resource) - { - var permissionResult = ContentPermissionsHelper.CheckPermissions(resource, - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, - _userService, - _entityService, - new[] { requirement.PermissionToCheck }); - - if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) - { - context.Fail(); - } - else - { - context.Succeed(requirement); - } - - return Task.CompletedTask; - } - } /// /// Used to authorize if the user has the correct permission access to the content for the content id specified in a query string @@ -59,21 +20,18 @@ namespace Umbraco.Web.BackOffice.Authorization private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IEntityService _entityService; - private readonly IUserService _userService; - private readonly IContentService _contentService; + private readonly ContentPermissions _contentPermissions; public ContentPermissionQueryStringHandler( IBackOfficeSecurityAccessor backofficeSecurityAccessor, IHttpContextAccessor httpContextAccessor, IEntityService entityService, - IUserService userService, - IContentService contentService) + ContentPermissions contentPermissions) { _backofficeSecurityAccessor = backofficeSecurityAccessor; _httpContextAccessor = httpContextAccessor; _entityService = entityService; - _userService = userService; - _contentService = contentService; + _contentPermissions = contentPermissions; } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionsQueryStringRequirement requirement) @@ -118,20 +76,17 @@ namespace Umbraco.Web.BackOffice.Authorization nodeId = requirement.NodeId.Value; } - var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, - _userService, - _contentService, - _entityService, - out var contentItem, + var permissionResult = _contentPermissions.CheckPermissions(nodeId, + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + out IContent contentItem, new[] { requirement.PermissionToCheck }); - if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound) + if (permissionResult == ContentPermissions.ContentAccess.NotFound) { return null; } - if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) + if (permissionResult == ContentPermissions.ContentAccess.Denied) { context.Fail(); } diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceHandler.cs new file mode 100644 index 0000000000..8172eab2a7 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceHandler.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Core.Security; +using Umbraco.Core.Services; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Used to authorize if the user has the correct permission access to the content for the specified + /// + public class ContentPermissionResourceHandler : AuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly ContentPermissions _contentPermissions; + + public ContentPermissionResourceHandler( + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + ContentPermissions contentPermissions) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _contentPermissions = contentPermissions; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionResourceRequirement requirement, IContent resource) + { + var permissionResult = _contentPermissions.CheckPermissions(resource, + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + requirement.PermissionsToCheck); + + if (permissionResult == ContentPermissions.ContentAccess.Denied) + { + context.Fail(); + } + else + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceRequirement.cs index 6781f45ab5..f430ffcd60 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceRequirement.cs @@ -1,7 +1,10 @@ using Microsoft.AspNetCore.Authorization; +using System.Collections.Generic; +using Umbraco.Web.Actions; namespace Umbraco.Web.BackOffice.Authorization { + /// /// An authorization requirement for /// @@ -13,9 +16,21 @@ namespace Umbraco.Web.BackOffice.Authorization /// public ContentPermissionResourceRequirement(char permissionToCheck) { - PermissionToCheck = permissionToCheck; + PermissionsToCheck = new List { permissionToCheck }; } - public char PermissionToCheck { get; } + public ContentPermissionResourceRequirement(IReadOnlyList permissionToCheck) + { + PermissionsToCheck = permissionToCheck; + } + + public ContentPermissionResourceRequirement(int nodeId, IReadOnlyList permissionToCheck) + { + NodeId = nodeId; + PermissionsToCheck = permissionToCheck; + } + + public int? NodeId { get; } + public IReadOnlyList PermissionsToCheck { get; } } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchRequirement.cs new file mode 100644 index 0000000000..6c56b828bc --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchRequirement.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Authorization requirement for + /// + public class ContentPermissionsPublishBranchRequirement : IAuthorizationRequirement + { + public ContentPermissionsPublishBranchRequirement(char permission) + { + Permission = permission; + } + + public char Permission { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionQueryStringHandler.cs new file mode 100644 index 0000000000..dc3cdf3090 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionQueryStringHandler.cs @@ -0,0 +1,94 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using System; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Security; +using Umbraco.Core.Services; + +namespace Umbraco.Web.BackOffice.Authorization +{ + public class MediaPermissionQueryStringHandler : AuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly MediaPermissions _mediaPermissions; + private readonly IEntityService _entityService; + + public MediaPermissionQueryStringHandler( + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IHttpContextAccessor httpContextAccessor, + MediaPermissions mediaPermissions) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _httpContextAccessor = httpContextAccessor; + _mediaPermissions = mediaPermissions; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MediaPermissionsQueryStringRequirement requirement) + { + StringValues routeVal; + foreach (var qs in requirement.QueryStringNames) + { + if (_httpContextAccessor.HttpContext.Request.Query.TryGetValue(qs, out routeVal)) + { + break; + } + } + + if (routeVal.Count == 0) + { + throw new InvalidOperationException("No argument found for the current action with by names " + string.Join(", ", requirement.QueryStringNames)); + } + + int nodeId; + + var argument = routeVal.ToString(); + // if the argument is an int, it will parse and can be assigned to nodeId + // if might be a udi, so check that next + // otherwise treat it as a guid - unlikely we ever get here + if (int.TryParse(argument, out int parsedId)) + { + nodeId = parsedId; + } + else if (UdiParser.TryParse(argument, true, out var udi)) + { + nodeId = _entityService.GetId(udi).Result; + } + else + { + Guid.TryParse(argument, out Guid key); + nodeId = _entityService.GetId(key, UmbracoObjectTypes.Document).Result; + } + + var permissionResult = _mediaPermissions.CheckPermissions( + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + nodeId, + out var mediaItem); + + if (permissionResult == MediaPermissions.MediaAccess.NotFound) + { + return null; + } + + if (permissionResult == MediaPermissions.MediaAccess.Denied) + { + context.Fail(); + } + else + { + context.Succeed(requirement); + } + + if (mediaItem != null) + { + //store the content item in request cache so it can be resolved in the controller without re-looking it up + _httpContextAccessor.HttpContext.Items[typeof(IMedia).ToString()] = mediaItem; + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceHandler.cs new file mode 100644 index 0000000000..074cb4e51b --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceHandler.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Core.Security; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Used to authorize if the user has the correct permission access to the content for the specified + /// + public class MediaPermissionResourceHandler : AuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly MediaPermissions _mediaPermissions; + + public MediaPermissionResourceHandler( + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + MediaPermissions mediaPermissions) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _mediaPermissions = mediaPermissions; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MediaPermissionResourceRequirement requirement, IMedia resource) + { + var permissionResult = MediaPermissions.MediaAccess.NotFound; + + if (resource != null) + { + permissionResult = _mediaPermissions.CheckPermissions( + resource, + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); + } + else if (requirement.NodeId.HasValue) + { + permissionResult = _mediaPermissions.CheckPermissions( + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + requirement.NodeId.Value, + out _); + } + + if (permissionResult == MediaPermissions.MediaAccess.Denied) + { + context.Fail(); + } + else + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceRequirement.cs new file mode 100644 index 0000000000..5721a57d62 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceRequirement.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// An authorization requirement for + /// + public class MediaPermissionResourceRequirement : IAuthorizationRequirement + { + public MediaPermissionResourceRequirement() + { + } + + public MediaPermissionResourceRequirement(int nodeId) + { + NodeId = nodeId; + } + + public int? NodeId { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringRequirement.cs new file mode 100644 index 0000000000..75bb78cea1 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringRequirement.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + public class MediaPermissionsQueryStringRequirement : IAuthorizationRequirement + { + public MediaPermissionsQueryStringRequirement(string[] paramNames) + { + QueryStringNames = paramNames; + } + + public string[] QueryStringNames { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 06f6d3b052..ba5016251d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -638,9 +638,9 @@ namespace Umbraco.Web.BackOffice.Controllers /// [FileUploadCleanupFilter] [ContentSaveValidation] - public ContentItemDisplay PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) + public async Task PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { - var contentItemDisplay = PostSaveInternal(contentItem, + var contentItemDisplay = await PostSaveInternal(contentItem, content => { EnsureUniqueName(content.Name, content, "Name"); @@ -666,9 +666,9 @@ namespace Umbraco.Web.BackOffice.Controllers [FileUploadCleanupFilter] [ContentSaveValidation] [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - public ContentItemDisplay PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) + public async Task PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { - var contentItemDisplay = PostSaveInternal( + var contentItemDisplay = await PostSaveInternal( contentItem, content => _contentService.Save(contentItem.PersistedContent, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id), MapToDisplay); @@ -676,7 +676,7 @@ namespace Umbraco.Web.BackOffice.Controllers return contentItemDisplay; } - private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func saveMethod, Func mapToDisplay) + private async Task PostSaveInternal(ContentItemSave contentItem, Func saveMethod, Func mapToDisplay) { //Recent versions of IE/Edge may send in the full client side file path instead of just the file name. //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all @@ -811,7 +811,7 @@ namespace Umbraco.Web.BackOffice.Controllers case ContentSaveAction.PublishWithDescendants: case ContentSaveAction.PublishWithDescendantsNew: { - if (!ValidatePublishBranchPermissions(contentItem, out var noAccess)) + if (!await ValidatePublishBranchPermissionsAsync(contentItem)) { globalNotifications.AddErrorNotification( _localizedTextService.Localize("publish"), @@ -827,7 +827,7 @@ namespace Umbraco.Web.BackOffice.Controllers case ContentSaveAction.PublishWithDescendantsForce: case ContentSaveAction.PublishWithDescendantsForceNew: { - if (!ValidatePublishBranchPermissions(contentItem, out var noAccess)) + if (!await ValidatePublishBranchPermissionsAsync(contentItem)) { globalNotifications.AddErrorNotification( _localizedTextService.Localize("publish"), @@ -1198,33 +1198,12 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - private bool ValidatePublishBranchPermissions(ContentItemSave contentItem, out IReadOnlyList noAccess) + private async Task ValidatePublishBranchPermissionsAsync(ContentItemSave contentItem) { - var denied = new List(); - var page = 0; - const int pageSize = 500; - var total = long.MaxValue; - while (page * pageSize < total) - { - var descendants = _entityService.GetPagedDescendants(contentItem.Id, UmbracoObjectTypes.Document, page++, pageSize, out total, - //order by shallowest to deepest, this allows us to check permissions from top to bottom so we can exit - //early if a permission higher up fails - ordering: Ordering.By("path", Direction.Ascending)); - - foreach (var c in descendants) - { - //if this item's path has already been denied or if the user doesn't have access to it, add to the deny list - if (denied.Any(x => c.Path.StartsWith($"{x.Path},")) - || (ContentPermissionsHelper.CheckPermissions(c, - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, _userService, _entityService, - ActionPublish.ActionLetter) == ContentPermissionsHelper.ContentAccess.Denied)) - { - denied.Add(c); - } - } - } - noAccess = denied; - return denied.Count == 0; + // Authorize... + var requirement = new ContentPermissionsPublishBranchRequirement(ActionPublish.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, contentItem.PersistedContent, requirement); + return authorizationResult.Succeeded; } private IEnumerable PublishBranchInternal(ContentItemSave contentItem, bool force, string cultureForInvariantErrors, diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 4c68b7dfa6..f98d09bd84 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -42,6 +42,7 @@ using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; using Microsoft.AspNetCore.Authorization; using Umbraco.Web.Common.Authorization; +using Umbraco.Web.BackOffice.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -67,6 +68,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IRelationService _relationService; private readonly IImageUrlGenerator _imageUrlGenerator; private readonly IJsonSerializer _serializer; + private readonly IAuthorizationService _authorizationService; private readonly ILogger _logger; public MediaController( @@ -89,7 +91,8 @@ namespace Umbraco.Web.BackOffice.Controllers IMediaFileSystem mediaFileSystem, IHostingEnvironment hostingEnvironment, IImageUrlGenerator imageUrlGenerator, - IJsonSerializer serializer) + IJsonSerializer serializer, + IAuthorizationService authorizationService) : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, serializer) { _shortStringHelper = shortStringHelper; @@ -110,6 +113,7 @@ namespace Umbraco.Web.BackOffice.Controllers _logger = loggerFactory.CreateLogger(); _imageUrlGenerator = imageUrlGenerator; _serializer = serializer; + _authorizationService = authorizationService; } /// @@ -167,7 +171,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [EnsureUserPermissionForMedia("id")] + [Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)] [DetermineAmbiguousActionByPassingParameters] public MediaItemDisplay GetById(int id) { @@ -188,7 +192,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [EnsureUserPermissionForMedia("id")] + [Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)] [DetermineAmbiguousActionByPassingParameters] public MediaItemDisplay GetById(Guid id) { @@ -209,7 +213,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [EnsureUserPermissionForMedia("id")] + [Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)] [DetermineAmbiguousActionByPassingParameters] public MediaItemDisplay GetById(Udi id) { @@ -432,7 +436,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [EnsureUserPermissionForMedia("id")] + [Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)] [HttpPost] public IActionResult DeleteById(int id) { @@ -473,9 +477,16 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [EnsureUserPermissionForMedia("move.Id")] - public IActionResult PostMove(MoveOrCopy move) + public async Task PostMove(MoveOrCopy move) { + // Authorize... + var requirement = new MediaPermissionResourceRequirement(); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, _mediaService.GetById(move.Id), requirement); + if (!authorizationResult.Succeeded) + { + return Forbid(); + } + var toMove = ValidateMoveOrCopy(move); var destinationParentID = move.ParentId; var sourceParentID = toMove.ParentId; @@ -610,8 +621,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [EnsureUserPermissionForMedia("sorted.ParentId")] - public IActionResult PostSort(ContentSortOrder sorted) + public async Task PostSort(ContentSortOrder sorted) { if (sorted == null) { @@ -624,14 +634,21 @@ namespace Umbraco.Web.BackOffice.Controllers return Ok(); } - var mediaService = _mediaService; + // Authorize... + var requirement = new MediaPermissionResourceRequirement(); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, _mediaService.GetById(sorted.ParentId), requirement); + if (!authorizationResult.Succeeded) + { + return Forbid(); + } + var sortedMedia = new List(); try { - sortedMedia.AddRange(sorted.IdSortOrder.Select(mediaService.GetById)); + sortedMedia.AddRange(sorted.IdSortOrder.Select(_mediaService.GetById)); // Save Media with new sort order and update content xml in db accordingly - if (mediaService.Sort(sortedMedia) == false) + if (_mediaService.Sort(sortedMedia) == false) { _logger.LogWarning("Media sorting failed, this was probably caused by an event being cancelled"); throw HttpResponseException.CreateValidationErrorResponse("Media sorting failed, this was probably caused by an event being cancelled"); @@ -645,19 +662,16 @@ namespace Umbraco.Web.BackOffice.Controllers } } - public ActionResult PostAddFolder(PostedFolder folder) + public async Task> PostAddFolder(PostedFolder folder) { - var parentId = GetParentIdAsInt(folder.ParentId, validatePermissions:true); + var parentId = await GetParentIdAsIntAsync(folder.ParentId, validatePermissions:true); if (!parentId.HasValue) { return NotFound("The passed id doesn't exist"); } - - var mediaService = _mediaService; - - var f = mediaService.CreateMedia(folder.Name, parentId.Value, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + var f = _mediaService.CreateMedia(folder.Name, parentId.Value, Constants.Conventions.MediaTypes.Folder); + _mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); return _umbracoMapper.Map(f); } @@ -682,13 +696,13 @@ namespace Umbraco.Web.BackOffice.Controllers } //get the string json from the request - var parentId = GetParentIdAsInt(currentFolder, validatePermissions: true); + var parentId = await GetParentIdAsIntAsync(currentFolder, validatePermissions: true); if (!parentId.HasValue) { return NotFound("The passed id doesn't exist"); } var tempFiles = new PostedFiles(); - var mediaService = _mediaService; + //in case we pass a path with a folder in it, we will create it and upload media to it. if (!string.IsNullOrEmpty(path)) @@ -706,18 +720,18 @@ namespace Umbraco.Web.BackOffice.Controllers { //look for matching folder folderMediaItem = - mediaService.GetRootMedia().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); + _mediaService.GetRootMedia().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); if (folderMediaItem == null) { //if null, create a folder - folderMediaItem = mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(folderMediaItem); + folderMediaItem = _mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder); + _mediaService.Save(folderMediaItem); } } else { //get current parent - var mediaRoot = mediaService.GetById(parentId.Value); + var mediaRoot = _mediaService.GetById(parentId.Value); //if the media root is null, something went wrong, we'll abort if (mediaRoot == null) @@ -731,8 +745,8 @@ namespace Umbraco.Web.BackOffice.Controllers if (folderMediaItem == null) { //if null, create a folder - folderMediaItem = mediaService.CreateMedia(folderName, mediaRoot, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(folderMediaItem); + folderMediaItem = _mediaService.CreateMedia(folderName, mediaRoot, Constants.Conventions.MediaTypes.Folder); + _mediaService.Save(folderMediaItem); } } //set the media root to the folder id so uploaded files will end there. @@ -765,7 +779,7 @@ namespace Umbraco.Web.BackOffice.Controllers var mediaItemName = fileName.ToFriendlyName(); - var f = mediaService.CreateMedia(mediaItemName, parentId.Value, mediaType, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + var f = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaType, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); await using (var stream = formFile.OpenReadStream()) @@ -774,7 +788,7 @@ namespace Umbraco.Web.BackOffice.Controllers } - var saveResult = mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + var saveResult = _mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); if (saveResult == false) { AddCancelMessage(tempFiles, @@ -830,7 +844,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// and if that check fails an unauthorized exception will occur /// /// - private int? GetParentIdAsInt(string parentId, bool validatePermissions) + private async Task GetParentIdAsIntAsync(string parentId, bool validatePermissions) { int intParentId; @@ -863,22 +877,23 @@ namespace Umbraco.Web.BackOffice.Controllers } } + // Authorize... //ensure the user has access to this folder by parent id! - if (validatePermissions && CheckPermissions( - new Dictionary(), - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, - _mediaService, - _entityService, - intParentId) == false) + if (validatePermissions) { - throw new HttpResponseException( + var requirement = new MediaPermissionResourceRequirement(); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, _mediaService.GetById(intParentId), requirement); + if (!authorizationResult.Succeeded) + { + throw new HttpResponseException( HttpStatusCode.Forbidden, new SimpleNotificationModel(new BackOfficeNotification( _localizedTextService.Localize("speechBubbles/operationFailedHeader"), _localizedTextService.Localize("speechBubbles/invalidUserPermissionsText"), NotificationStyle.Warning))); + } } - + return intParentId; } @@ -894,8 +909,8 @@ namespace Umbraco.Web.BackOffice.Controllers throw new HttpResponseException(HttpStatusCode.NotFound); } - var mediaService = _mediaService; - var toMove = mediaService.GetById(model.Id); + + var toMove = _mediaService.GetById(model.Id); if (toMove == null) { throw new HttpResponseException(HttpStatusCode.NotFound); @@ -914,7 +929,7 @@ namespace Umbraco.Web.BackOffice.Controllers } else { - var parent = mediaService.GetById(model.ParentId); + var parent = _mediaService.GetById(model.ParentId); if (parent == null) { throw new HttpResponseException(HttpStatusCode.NotFound); @@ -942,45 +957,7 @@ namespace Umbraco.Web.BackOffice.Controllers return toMove; } - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// The storage to add the content item to so it can be reused - /// - /// - /// - /// The content to lookup, if the contentItem is not specified - /// Specifies the already resolved content item to check against, setting this ignores the nodeId - /// - internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, IEntityService entityService, int nodeId, IMedia media = null) - { - if (storage == null) throw new ArgumentNullException("storage"); - if (user == null) throw new ArgumentNullException("user"); - if (mediaService == null) throw new ArgumentNullException("mediaService"); - if (entityService == null) throw new ArgumentNullException("entityService"); - - if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) - { - media = mediaService.GetById(nodeId); - //put the content item into storage so it can be retrieved - // in the controller (saves a lookup) - storage[typeof(IMedia).ToString()] = media; - } - - if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var hasPathAccess = (nodeId == Constants.System.Root) - ? user.HasMediaRootAccess(entityService) - : (nodeId == Constants.System.RecycleBinMedia) - ? user.HasMediaBinAccess(entityService) - : user.HasPathAccess(media, entityService); - - return hasPathAccess; - } + public PagedResult GetPagedReferences(int id, string entityType, int pageNumber = 1, int pageSize = 100) { diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 8e3eb96660..2aadaa3fd6 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -128,10 +128,22 @@ namespace Umbraco.Extensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddAuthorization(options => { + // these are the query strings we will check for media ids when permission checking + var mediaPermissionQueryStrings = new[] { "id" }; + + options.AddPolicy(AuthorizationPolicies.MediaPermissionPathById, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new MediaPermissionsQueryStringRequirement(mediaPermissionQueryStrings)); + }); + // these are the query strings we will check for content ids when permission checking var contentPermissionQueryStrings = new[] { "id", "contentId" }; diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index 2553232185..fba9fa9b35 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -10,6 +12,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Actions; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Security; @@ -27,11 +30,13 @@ namespace Umbraco.Web.BackOffice.Filters } - private sealed class ContentSaveValidationFilter : IActionFilter + private sealed class ContentSaveValidationFilter : IAsyncActionFilter { private readonly IContentService _contentService; private readonly IEntityService _entityService; private readonly IPropertyValidationService _propertyValidationService; + private readonly ContentPermissions _contentPermissions; + private readonly IAuthorizationService _authorizationService; private readonly ILoggerFactory _loggerFactory; private readonly ILocalizedTextService _textService; private readonly IUserService _userService; @@ -45,7 +50,9 @@ namespace Umbraco.Web.BackOffice.Filters IContentService contentService, IUserService userService, IEntityService entityService, - IPropertyValidationService propertyValidationService) + IPropertyValidationService propertyValidationService, + ContentPermissions contentPermissions, + IAuthorizationService authorizationService) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); @@ -54,16 +61,28 @@ namespace Umbraco.Web.BackOffice.Filters _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); _propertyValidationService = propertyValidationService ?? throw new ArgumentNullException(nameof(propertyValidationService)); + _contentPermissions = contentPermissions; + _authorizationService = authorizationService; } - public void OnActionExecuting(ActionExecutingContext context) + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // on executing... + await OnActionExecutingAsync(context); + + await next(); //need to pass the execution to next + + // on executed... + } + + private async Task OnActionExecutingAsync(ActionExecutingContext context) { var model = (ContentItemSave) context.ActionArguments["contentItem"]; var contentItemValidator = new ContentSaveModelValidator(_loggerFactory.CreateLogger(), _backofficeSecurityAccessor.BackOfficeSecurity, _textService, _propertyValidationService); if (!ValidateAtLeastOneVariantIsBeingSaved(model, context)) return; if (!contentItemValidator.ValidateExistingContent(model, context)) return; - if (!ValidateUserAccess(model, context, _backofficeSecurityAccessor.BackOfficeSecurity)) return; + if (!await ValidateUserAccessAsync(model, context, _backofficeSecurityAccessor.BackOfficeSecurity)) return; //validate for each variant that is being updated foreach (var variant in model.Variants.Where(x => x.Save)) @@ -74,9 +93,6 @@ namespace Umbraco.Web.BackOffice.Filters } } - public void OnActionExecuted(ActionExecutedContext context) - { - } /// /// If there are no variants tagged for Saving, then this is an invalid request @@ -84,7 +100,8 @@ namespace Umbraco.Web.BackOffice.Filters /// /// /// - private bool ValidateAtLeastOneVariantIsBeingSaved(ContentItemSave contentItem, + private bool ValidateAtLeastOneVariantIsBeingSaved( + ContentItemSave contentItem, ActionExecutingContext actionContext) { if (!contentItem.Variants.Any(x => x.Save)) @@ -102,7 +119,9 @@ namespace Umbraco.Web.BackOffice.Filters /// /// /// - private bool ValidateUserAccess(ContentItemSave contentItem, ActionExecutingContext actionContext, + private async Task ValidateUserAccessAsync( + ContentItemSave contentItem, + ActionExecutingContext actionContext, IBackOfficeSecurity backofficeSecurity) { // We now need to validate that the user is allowed to be doing what they are doing. @@ -210,42 +229,22 @@ namespace Umbraco.Web.BackOffice.Filters throw new ArgumentOutOfRangeException(); } - ContentPermissionsHelper.ContentAccess accessResult; - if (contentToCheck != null) - { - //store the content item in request cache so it can be resolved in the controller without re-looking it up - actionContext.HttpContext.Items[typeof(IContent).ToString()] = contentItem; - accessResult = ContentPermissionsHelper.CheckPermissions( - contentToCheck, backofficeSecurity.CurrentUser, - _userService, _entityService, permissionToCheck.ToArray()); - } - else - { - accessResult = ContentPermissionsHelper.CheckPermissions( - contentIdToCheck, backofficeSecurity.CurrentUser, - _userService, _contentService, _entityService, - out contentToCheck, - permissionToCheck.ToArray()); - if (contentToCheck != null) - { - //store the content item in request cache so it can be resolved in the controller without re-looking it up - actionContext.HttpContext.Items[typeof(IContent).ToString()] = contentToCheck; - } - } + var requirement = contentToCheck == null + ? new ContentPermissionResourceRequirement(contentIdToCheck, permissionToCheck) + : new ContentPermissionResourceRequirement(permissionToCheck); - if (accessResult == ContentPermissionsHelper.ContentAccess.NotFound) - { - actionContext.Result = new NotFoundResult(); - } - - if (accessResult != ContentPermissionsHelper.ContentAccess.Granted) + var authorizationResult = await _authorizationService.AuthorizeAsync(actionContext.HttpContext.User, contentToCheck, requirement); + if (!authorizationResult.Succeeded) { actionContext.Result = new ForbidResult(); + return false; } - return accessResult == ContentPermissionsHelper.ContentAccess.Granted; + return true; } + + } } } diff --git a/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs index cabc865d40..607d77cd21 100644 --- a/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs @@ -1,182 +1,184 @@ -using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Controllers; -using Umbraco.Web.Common.Exceptions; -using Umbraco.Web.Editors; -using Umbraco.Web.Security; +//using System; +//using Microsoft.AspNetCore.Mvc; +//using Microsoft.AspNetCore.Mvc.Filters; +//using Umbraco.Core; +//using Umbraco.Core.Models; +//using Umbraco.Core.Security; +//using Umbraco.Core.Services; +//using Umbraco.Web.BackOffice.Controllers; +//using Umbraco.Web.Common.Exceptions; +//using Umbraco.Web.Editors; +//using Umbraco.Web.Security; -namespace Umbraco.Web.BackOffice.Filters -{ - /// - /// Auth filter to check if the current user has access to the content item - /// - /// - /// Since media doesn't have permissions, this simply checks start node access - /// - internal sealed class EnsureUserPermissionForMediaAttribute : TypeFilterAttribute - { - public EnsureUserPermissionForMediaAttribute(int nodeId) - : base(typeof(EnsureUserPermissionForMediaFilter)) - { - Arguments = new object[] - { - nodeId - }; - } +//namespace Umbraco.Web.BackOffice.Filters +//{ +// /// +// /// Auth filter to check if the current user has access to the content item +// /// +// /// +// /// Since media doesn't have permissions, this simply checks start node access +// /// +// internal sealed class EnsureUserPermissionForMediaAttribute : TypeFilterAttribute +// { +// // TODO: This needs to be an authorization policy - public EnsureUserPermissionForMediaAttribute(string paramName) - : base(typeof(EnsureUserPermissionForMediaFilter)) - { - Arguments = new object[] - { - paramName - }; - } - private sealed class EnsureUserPermissionForMediaFilter : IActionFilter - { - private readonly int? _nodeId; - private readonly string _paramName; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - private readonly IEntityService _entityService; - private readonly IMediaService _mediaService; +// public EnsureUserPermissionForMediaAttribute(int nodeId) +// : base(typeof(EnsureUserPermissionForMediaFilter)) +// { +// Arguments = new object[] +// { +// nodeId +// }; +// } - /// - /// This constructor will only be able to test the start node access - /// - public EnsureUserPermissionForMediaFilter( - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IEntityService entityService, - IMediaService mediaService, - int nodeId) - :this(backofficeSecurityAccessor, entityService, mediaService, nodeId, null) - { - _nodeId = nodeId; - } +// public EnsureUserPermissionForMediaAttribute(string paramName) +// : base(typeof(EnsureUserPermissionForMediaFilter)) +// { +// Arguments = new object[] +// { +// paramName +// }; +// } +// private sealed class EnsureUserPermissionForMediaFilter : IActionFilter +// { +// private readonly int? _nodeId; +// private readonly string _paramName; +// private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; +// private readonly IEntityService _entityService; +// private readonly IMediaService _mediaService; - public EnsureUserPermissionForMediaFilter( - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IEntityService entityService, - IMediaService mediaService, - string paramName) - :this(backofficeSecurityAccessor, entityService, mediaService,null, paramName) - { - if (paramName == null) throw new ArgumentNullException(nameof(paramName)); - if (string.IsNullOrEmpty(paramName)) - throw new ArgumentException("Value can't be empty.", nameof(paramName)); - } +// /// +// /// This constructor will only be able to test the start node access +// /// +// public EnsureUserPermissionForMediaFilter( +// IBackOfficeSecurityAccessor backofficeSecurityAccessor, +// IEntityService entityService, +// IMediaService mediaService, +// int nodeId) +// :this(backofficeSecurityAccessor, entityService, mediaService, nodeId, null) +// { +// _nodeId = nodeId; +// } - private EnsureUserPermissionForMediaFilter( - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - IEntityService entityService, - IMediaService mediaService, - int? nodeId, string paramName) - { - _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); - _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); +// public EnsureUserPermissionForMediaFilter( +// IBackOfficeSecurityAccessor backofficeSecurityAccessor, +// IEntityService entityService, +// IMediaService mediaService, +// string paramName) +// :this(backofficeSecurityAccessor, entityService, mediaService,null, paramName) +// { +// if (paramName == null) throw new ArgumentNullException(nameof(paramName)); +// if (string.IsNullOrEmpty(paramName)) +// throw new ArgumentException("Value can't be empty.", nameof(paramName)); +// } - _paramName = paramName; +// private EnsureUserPermissionForMediaFilter( +// IBackOfficeSecurityAccessor backofficeSecurityAccessor, +// IEntityService entityService, +// IMediaService mediaService, +// int? nodeId, string paramName) +// { +// _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); +// _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); +// _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); - if (nodeId.HasValue) - { - _nodeId = nodeId.Value; - } - } +// _paramName = paramName; - private int GetNodeIdFromParameter(object parameterValue) - { - if (parameterValue is int) - { - return (int) parameterValue; - } +// if (nodeId.HasValue) +// { +// _nodeId = nodeId.Value; +// } +// } - var guidId = Guid.Empty; - if (parameterValue is Guid) - { - guidId = (Guid) parameterValue; - } - else if (parameterValue is GuidUdi) - { - guidId = ((GuidUdi) parameterValue).Guid; - } +// private int GetNodeIdFromParameter(object parameterValue) +// { +// if (parameterValue is int) +// { +// return (int) parameterValue; +// } - if (guidId != Guid.Empty) - { - var found = _entityService.GetId(guidId, UmbracoObjectTypes.Media); - if (found) - return found.Result; - } +// var guidId = Guid.Empty; +// if (parameterValue is Guid) +// { +// guidId = (Guid) parameterValue; +// } +// else if (parameterValue is GuidUdi) +// { +// guidId = ((GuidUdi) parameterValue).Guid; +// } - throw new InvalidOperationException("The id type: " + parameterValue.GetType() + - " is not a supported id"); - } +// if (guidId != Guid.Empty) +// { +// var found = _entityService.GetId(guidId, UmbracoObjectTypes.Media); +// if (found) +// return found.Result; +// } - public void OnActionExecuting(ActionExecutingContext context) - { - if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser == null) - { - throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); - } +// throw new InvalidOperationException("The id type: " + parameterValue.GetType() + +// " is not a supported id"); +// } - int nodeId; - if (_nodeId.HasValue == false) - { - var parts = _paramName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); +// public void OnActionExecuting(ActionExecutingContext context) +// { +// if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser == null) +// { +// throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); +// } - if (context.ActionArguments[parts[0]] == null) - { - throw new InvalidOperationException("No argument found for the current action with the name: " + - _paramName); - } +// int nodeId; +// if (_nodeId.HasValue == false) +// { +// var parts = _paramName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 1) - { - nodeId = GetNodeIdFromParameter(context.ActionArguments[parts[0]]); - } - else - { - //now we need to see if we can get the property of whatever object it is - var pType = context.ActionArguments[parts[0]].GetType(); - var prop = pType.GetProperty(parts[1]); - if (prop == null) - { - throw new InvalidOperationException( - "No argument found for the current action with the name: " + _paramName); - } +// if (context.ActionArguments[parts[0]] == null) +// { +// throw new InvalidOperationException("No argument found for the current action with the name: " + +// _paramName); +// } - nodeId = GetNodeIdFromParameter(prop.GetValue(context.ActionArguments[parts[0]])); - } - } - else - { - nodeId = _nodeId.Value; - } +// if (parts.Length == 1) +// { +// nodeId = GetNodeIdFromParameter(context.ActionArguments[parts[0]]); +// } +// else +// { +// //now we need to see if we can get the property of whatever object it is +// var pType = context.ActionArguments[parts[0]].GetType(); +// var prop = pType.GetProperty(parts[1]); +// if (prop == null) +// { +// throw new InvalidOperationException( +// "No argument found for the current action with the name: " + _paramName); +// } - if (MediaController.CheckPermissions( - context.HttpContext.Items, - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, - _mediaService, - _entityService, - nodeId)) - { +// nodeId = GetNodeIdFromParameter(prop.GetValue(context.ActionArguments[parts[0]])); +// } +// } +// else +// { +// nodeId = _nodeId.Value; +// } - } - else - { - throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); - } - } +// if (MediaController.CheckPermissions( +// context.HttpContext.Items, +// _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, +// _mediaService, +// _entityService, +// nodeId)) +// { - public void OnActionExecuted(ActionExecutedContext context) - { +// } +// else +// { +// throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); +// } +// } - } +// public void OnActionExecuted(ActionExecutedContext context) +// { - } - } -} +// } + +// } +// } +//} diff --git a/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs index 679fcdad6b..c7a7a56f83 100644 --- a/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -87,7 +87,7 @@ namespace Umbraco.Web.BackOffice.Filters var toRemove = new List(); foreach (dynamic item in items) { - var hasPathAccess = (item != null && ContentPermissionsHelper.HasPathAccess(item.Path, GetUserStartNodes(user), RecycleBinId)); + var hasPathAccess = (item != null && ContentPermissions.HasPathAccess(item.Path, GetUserStartNodes(user), RecycleBinId)); if (hasPathAccess == false) { toRemove.Add(item); diff --git a/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs index fa1ee568f0..afedf44211 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs @@ -1,4 +1,6 @@ using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; @@ -6,9 +8,8 @@ using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Security; using Umbraco.Core.Services; -using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.BackOffice.Authorization; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Filters { @@ -21,12 +22,10 @@ namespace Umbraco.Web.BackOffice.Filters { } - private sealed class MediaItemSaveValidationFilter : IActionFilter + private sealed class MediaItemSaveValidationFilter : IAsyncActionFilter { - private readonly IEntityService _entityService; private readonly IPropertyValidationService _propertyValidationService; - - + private readonly IAuthorizationService _authorizationService; private readonly IMediaService _mediaService; private readonly ILocalizedTextService _textService; private readonly ILoggerFactory _loggerFactory; @@ -37,23 +36,33 @@ namespace Umbraco.Web.BackOffice.Filters IBackOfficeSecurityAccessor backofficeSecurityAccessor, ILocalizedTextService textService, IMediaService mediaService, - IEntityService entityService, - IPropertyValidationService propertyValidationService) + IPropertyValidationService propertyValidationService, + IAuthorizationService authorizationService) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); _propertyValidationService = propertyValidationService ?? throw new ArgumentNullException(nameof(propertyValidationService)); + _authorizationService = authorizationService ?? throw new ArgumentNullException(nameof(authorizationService)); } - public void OnActionExecuting(ActionExecutingContext context) + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // on executing... + await OnActionExecutingAsync(context); + + await next(); //need to pass the execution to next + + // on executed... + } + + private async Task OnActionExecutingAsync(ActionExecutingContext context) { var model = (MediaItemSave) context.ActionArguments["contentItem"]; var contentItemValidator = new MediaSaveModelValidator(_loggerFactory.CreateLogger(), _backofficeSecurityAccessor.BackOfficeSecurity, _textService, _propertyValidationService); - if (ValidateUserAccess(model, context)) + if (await ValidateUserAccessAsync(model, context)) { //now do each validation step if (contentItemValidator.ValidateExistingContent(model, context)) @@ -68,7 +77,7 @@ namespace Umbraco.Web.BackOffice.Filters /// /// /// - private bool ValidateUserAccess(MediaItemSave mediaItem, ActionExecutingContext actionContext) + private async Task ValidateUserAccessAsync(MediaItemSave mediaItem, ActionExecutingContext actionContext) { //We now need to validate that the user is allowed to be doing what they are doing. //Then if it is new, we need to lookup those permissions on the parent. @@ -100,11 +109,12 @@ namespace Umbraco.Web.BackOffice.Filters return false; } - if (MediaController.CheckPermissions( - actionContext.HttpContext.Items, - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, - _mediaService, _entityService, - contentIdToCheck, contentToCheck) == false) + var requirement = contentToCheck == null + ? new MediaPermissionResourceRequirement(contentIdToCheck) + : new MediaPermissionResourceRequirement(); + + var authorizationResult = await _authorizationService.AuthorizeAsync(actionContext.HttpContext.User, contentToCheck, requirement); + if (!authorizationResult.Succeeded) { actionContext.Result = new ForbidResult(); return false; @@ -112,11 +122,6 @@ namespace Umbraco.Web.BackOffice.Filters return true; } - - public void OnActionExecuted(ActionExecutedContext context) - { - - } } } } diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs index 36b9f7f5de..53a6f02a79 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs @@ -123,7 +123,7 @@ namespace Umbraco.Web.BackOffice.Trees internal TreeNode GetSingleTreeNodeWithAccessCheck(IEntitySlim e, string parentId, FormCollection queryStrings, int[] startNodeIds, string[] startNodePaths) { - var entityIsAncestorOfStartNodes = ContentPermissionsHelper.IsInBranchOfStartNode(e.Path, startNodeIds, startNodePaths, out var hasPathAccess); + var entityIsAncestorOfStartNodes = ContentPermissions.IsInBranchOfStartNode(e.Path, startNodeIds, startNodePaths, out var hasPathAccess); var ignoreUserStartNodes = IgnoreUserStartNodes(queryStrings); if (ignoreUserStartNodes == false && entityIsAncestorOfStartNodes == false) return null; diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index 644ee4dd71..335dc5397b 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -23,6 +23,8 @@ public const string ContentPermissionBrowseById = nameof(ContentPermissionBrowseById); public const string ContentPermissionDeleteById = nameof(ContentPermissionDeleteById); + public const string MediaPermissionPathById = nameof(MediaPermissionPathById); + // Single section access public const string SectionAccessContent = nameof(SectionAccessContent); diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index 8acea23289..a6c13bf50b 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -96,6 +96,9 @@ namespace Umbraco.Web.Common.Runtime composition.Services.AddUnique(); composition.Services.AddUnique(); composition.Services.AddUnique(factory => new LegacyPasswordSecurity()); + + composition.Services.AddUnique(); + composition.Services.AddUnique(); } } } From bde025f6616981fdf15f70d06afe9b100567011f Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Nov 2020 00:42:52 +1100 Subject: [PATCH 14/25] streamlines the naming of the content/media permissions handlers --- ...er.cs => ContentPermissionsPublishBranchHandler.cs} | 4 ++-- .../ContentPermissionsPublishBranchRequirement.cs | 2 +- ...dler.cs => ContentPermissionsQueryStringHandler.cs} | 4 ++-- .../ContentPermissionsQueryStringRequirement.cs | 2 +- ...Handler.cs => ContentPermissionsResourceHandler.cs} | 6 +++--- ...ent.cs => ContentPermissionsResourceRequirement.cs} | 10 +++++----- ...andler.cs => MediaPermissionsQueryStringHandler.cs} | 4 ++-- ...ceHandler.cs => MediaPermissionsResourceHandler.cs} | 6 +++--- ...ement.cs => MediaPermissionsResourceRequirement.cs} | 8 ++++---- .../Controllers/ContentController.cs | 10 +++++----- .../Controllers/MediaController.cs | 6 +++--- .../BackOfficeServiceCollectionExtensions.cs | 10 +++++----- .../Filters/ContentSaveValidationAttribute.cs | 4 ++-- .../Filters/MediaItemSaveValidationAttribute.cs | 4 ++-- 14 files changed, 40 insertions(+), 40 deletions(-) rename src/Umbraco.Web.BackOffice/Authorization/{ContentPermissionPublishBranchHandler.cs => ContentPermissionsPublishBranchHandler.cs} (93%) rename src/Umbraco.Web.BackOffice/Authorization/{ContentPermissionQueryStringHandler.cs => ContentPermissionsQueryStringHandler.cs} (95%) rename src/Umbraco.Web.BackOffice/Authorization/{ContentPermissionResourceHandler.cs => ContentPermissionsResourceHandler.cs} (83%) rename src/Umbraco.Web.BackOffice/Authorization/{ContentPermissionResourceRequirement.cs => ContentPermissionsResourceRequirement.cs} (67%) rename src/Umbraco.Web.BackOffice/Authorization/{MediaPermissionQueryStringHandler.cs => MediaPermissionsQueryStringHandler.cs} (95%) rename src/Umbraco.Web.BackOffice/Authorization/{MediaPermissionResourceHandler.cs => MediaPermissionsResourceHandler.cs} (86%) rename src/Umbraco.Web.BackOffice/Authorization/{MediaPermissionResourceRequirement.cs => MediaPermissionsResourceRequirement.cs} (59%) diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionPublishBranchHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandler.cs similarity index 93% rename from src/Umbraco.Web.BackOffice/Authorization/ContentPermissionPublishBranchHandler.cs rename to src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandler.cs index f172bfdea0..89e72f6cba 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionPublishBranchHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandler.cs @@ -13,13 +13,13 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// The user must have access to all descendant nodes of the content item in order to continue /// - public class ContentPermissionPublishBranchHandler : AuthorizationHandler + public class ContentPermissionsPublishBranchHandler : AuthorizationHandler { private readonly IEntityService _entityService; private readonly ContentPermissions _contentPermissions; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - public ContentPermissionPublishBranchHandler( + public ContentPermissionsPublishBranchHandler( IEntityService entityService, ContentPermissions contentPermissions, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchRequirement.cs index 6c56b828bc..541f861f0d 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchRequirement.cs @@ -3,7 +3,7 @@ namespace Umbraco.Web.BackOffice.Authorization { /// - /// Authorization requirement for + /// Authorization requirement for /// public class ContentPermissionsPublishBranchRequirement : IAuthorizationRequirement { diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs similarity index 95% rename from src/Umbraco.Web.BackOffice/Authorization/ContentPermissionQueryStringHandler.cs rename to src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs index fa10d5477b..0534b92fc9 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs @@ -15,14 +15,14 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Used to authorize if the user has the correct permission access to the content for the content id specified in a query string /// - public class ContentPermissionQueryStringHandler : AuthorizationHandler + public class ContentPermissionsQueryStringHandler : AuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IEntityService _entityService; private readonly ContentPermissions _contentPermissions; - public ContentPermissionQueryStringHandler( + public ContentPermissionsQueryStringHandler( IBackOfficeSecurityAccessor backofficeSecurityAccessor, IHttpContextAccessor httpContextAccessor, IEntityService entityService, diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs index 9d2e931495..9153427614 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs @@ -4,7 +4,7 @@ namespace Umbraco.Web.BackOffice.Authorization { /// - /// An authorization requirement for + /// An authorization requirement for /// public class ContentPermissionsQueryStringRequirement : IAuthorizationRequirement { diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs similarity index 83% rename from src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceHandler.cs rename to src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs index 8172eab2a7..b2aeecb0ac 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs @@ -9,12 +9,12 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Used to authorize if the user has the correct permission access to the content for the specified /// - public class ContentPermissionResourceHandler : AuthorizationHandler + public class ContentPermissionsResourceHandler : AuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly ContentPermissions _contentPermissions; - public ContentPermissionResourceHandler( + public ContentPermissionsResourceHandler( IBackOfficeSecurityAccessor backofficeSecurityAccessor, ContentPermissions contentPermissions) { @@ -22,7 +22,7 @@ namespace Umbraco.Web.BackOffice.Authorization _contentPermissions = contentPermissions; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionResourceRequirement requirement, IContent resource) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionsResourceRequirement requirement, IContent resource) { var permissionResult = _contentPermissions.CheckPermissions(resource, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceRequirement.cs similarity index 67% rename from src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceRequirement.cs rename to src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceRequirement.cs index f430ffcd60..ca29362acf 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionResourceRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceRequirement.cs @@ -6,25 +6,25 @@ namespace Umbraco.Web.BackOffice.Authorization { /// - /// An authorization requirement for + /// An authorization requirement for /// - public class ContentPermissionResourceRequirement : IAuthorizationRequirement + public class ContentPermissionsResourceRequirement : IAuthorizationRequirement { /// /// Create an authorization requirement for a resource /// /// - public ContentPermissionResourceRequirement(char permissionToCheck) + public ContentPermissionsResourceRequirement(char permissionToCheck) { PermissionsToCheck = new List { permissionToCheck }; } - public ContentPermissionResourceRequirement(IReadOnlyList permissionToCheck) + public ContentPermissionsResourceRequirement(IReadOnlyList permissionToCheck) { PermissionsToCheck = permissionToCheck; } - public ContentPermissionResourceRequirement(int nodeId, IReadOnlyList permissionToCheck) + public ContentPermissionsResourceRequirement(int nodeId, IReadOnlyList permissionToCheck) { NodeId = nodeId; PermissionsToCheck = permissionToCheck; diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs similarity index 95% rename from src/Umbraco.Web.BackOffice/Authorization/MediaPermissionQueryStringHandler.cs rename to src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs index dc3cdf3090..09983064ad 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs @@ -10,14 +10,14 @@ using Umbraco.Core.Services; namespace Umbraco.Web.BackOffice.Authorization { - public class MediaPermissionQueryStringHandler : AuthorizationHandler + public class MediaPermissionsQueryStringHandler : AuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly IHttpContextAccessor _httpContextAccessor; private readonly MediaPermissions _mediaPermissions; private readonly IEntityService _entityService; - public MediaPermissionQueryStringHandler( + public MediaPermissionsQueryStringHandler( IBackOfficeSecurityAccessor backofficeSecurityAccessor, IHttpContextAccessor httpContextAccessor, MediaPermissions mediaPermissions) diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs similarity index 86% rename from src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceHandler.cs rename to src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs index 074cb4e51b..34451ade11 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs @@ -8,12 +8,12 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Used to authorize if the user has the correct permission access to the content for the specified /// - public class MediaPermissionResourceHandler : AuthorizationHandler + public class MediaPermissionsResourceHandler : AuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly MediaPermissions _mediaPermissions; - public MediaPermissionResourceHandler( + public MediaPermissionsResourceHandler( IBackOfficeSecurityAccessor backofficeSecurityAccessor, MediaPermissions mediaPermissions) { @@ -21,7 +21,7 @@ namespace Umbraco.Web.BackOffice.Authorization _mediaPermissions = mediaPermissions; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MediaPermissionResourceRequirement requirement, IMedia resource) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MediaPermissionsResourceRequirement requirement, IMedia resource) { var permissionResult = MediaPermissions.MediaAccess.NotFound; diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceRequirement.cs similarity index 59% rename from src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceRequirement.cs rename to src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceRequirement.cs index 5721a57d62..0cd51c9c15 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionResourceRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceRequirement.cs @@ -3,15 +3,15 @@ namespace Umbraco.Web.BackOffice.Authorization { /// - /// An authorization requirement for + /// An authorization requirement for /// - public class MediaPermissionResourceRequirement : IAuthorizationRequirement + public class MediaPermissionsResourceRequirement : IAuthorizationRequirement { - public MediaPermissionResourceRequirement() + public MediaPermissionsResourceRequirement() { } - public MediaPermissionResourceRequirement(int nodeId) + public MediaPermissionsResourceRequirement(int nodeId) { NodeId = nodeId; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index ba5016251d..e9f6e6b8cb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -172,7 +172,7 @@ namespace Umbraco.Web.BackOffice.Controllers if (content == null) return NotFound(); // Authorize... - var requirement = new ContentPermissionResourceRequirement(ActionRights.ActionLetter); + var requirement = new ContentPermissionsResourceRequirement(ActionRights.ActionLetter); var authorizationResult = await _authorizationService.AuthorizeAsync(User, content, requirement); if (!authorizationResult.Succeeded) { @@ -1601,7 +1601,7 @@ namespace Umbraco.Web.BackOffice.Controllers } // Authorize... - var requirement = new ContentPermissionResourceRequirement(ActionSort.ActionLetter); + var requirement = new ContentPermissionsResourceRequirement(ActionSort.ActionLetter); var authorizationResult = await _authorizationService.AuthorizeAsync(User, _contentService.GetById(sorted.ParentId), requirement); if (!authorizationResult.Succeeded) { @@ -1636,7 +1636,7 @@ namespace Umbraco.Web.BackOffice.Controllers public async Task PostMove(MoveOrCopy move) { // Authorize... - var requirement = new ContentPermissionResourceRequirement(ActionMove.ActionLetter); + var requirement = new ContentPermissionsResourceRequirement(ActionMove.ActionLetter); var authorizationResult = await _authorizationService.AuthorizeAsync(User, _contentService.GetById(move.ParentId), requirement); if (!authorizationResult.Succeeded) { @@ -1658,7 +1658,7 @@ namespace Umbraco.Web.BackOffice.Controllers public async Task PostCopy(MoveOrCopy copy) { // Authorize... - var requirement = new ContentPermissionResourceRequirement(ActionCopy.ActionLetter); + var requirement = new ContentPermissionsResourceRequirement(ActionCopy.ActionLetter); var authorizationResult = await _authorizationService.AuthorizeAsync(User, _contentService.GetById(copy.ParentId), requirement); if (!authorizationResult.Succeeded) { @@ -1688,7 +1688,7 @@ namespace Umbraco.Web.BackOffice.Controllers } // Authorize... - var requirement = new ContentPermissionResourceRequirement(ActionUnpublish.ActionLetter); + var requirement = new ContentPermissionsResourceRequirement(ActionUnpublish.ActionLetter); var authorizationResult = await _authorizationService.AuthorizeAsync(User, foundContent, requirement); if (!authorizationResult.Succeeded) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index f98d09bd84..409967fb67 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -480,7 +480,7 @@ namespace Umbraco.Web.BackOffice.Controllers public async Task PostMove(MoveOrCopy move) { // Authorize... - var requirement = new MediaPermissionResourceRequirement(); + var requirement = new MediaPermissionsResourceRequirement(); var authorizationResult = await _authorizationService.AuthorizeAsync(User, _mediaService.GetById(move.Id), requirement); if (!authorizationResult.Succeeded) { @@ -635,7 +635,7 @@ namespace Umbraco.Web.BackOffice.Controllers } // Authorize... - var requirement = new MediaPermissionResourceRequirement(); + var requirement = new MediaPermissionsResourceRequirement(); var authorizationResult = await _authorizationService.AuthorizeAsync(User, _mediaService.GetById(sorted.ParentId), requirement); if (!authorizationResult.Succeeded) { @@ -881,7 +881,7 @@ namespace Umbraco.Web.BackOffice.Controllers //ensure the user has access to this folder by parent id! if (validatePermissions) { - var requirement = new MediaPermissionResourceRequirement(); + var requirement = new MediaPermissionsResourceRequirement(); var authorizationResult = await _authorizationService.AuthorizeAsync(User, _mediaService.GetById(intParentId), requirement); if (!authorizationResult.Succeeded) { diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 2aadaa3fd6..2c3cb13f6f 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -126,11 +126,11 @@ namespace Umbraco.Extensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddAuthorization(options => diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index fba9fa9b35..cefa8c7123 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -231,8 +231,8 @@ namespace Umbraco.Web.BackOffice.Filters var requirement = contentToCheck == null - ? new ContentPermissionResourceRequirement(contentIdToCheck, permissionToCheck) - : new ContentPermissionResourceRequirement(permissionToCheck); + ? new ContentPermissionsResourceRequirement(contentIdToCheck, permissionToCheck) + : new ContentPermissionsResourceRequirement(permissionToCheck); var authorizationResult = await _authorizationService.AuthorizeAsync(actionContext.HttpContext.User, contentToCheck, requirement); if (!authorizationResult.Succeeded) diff --git a/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs index afedf44211..b10043c73c 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs @@ -110,8 +110,8 @@ namespace Umbraco.Web.BackOffice.Filters } var requirement = contentToCheck == null - ? new MediaPermissionResourceRequirement(contentIdToCheck) - : new MediaPermissionResourceRequirement(); + ? new MediaPermissionsResourceRequirement(contentIdToCheck) + : new MediaPermissionsResourceRequirement(); var authorizationResult = await _authorizationService.AuthorizeAsync(actionContext.HttpContext.User, contentToCheck, requirement); if (!authorizationResult.Succeeded) From 5619b63ee45dde87a0de8a67019aba4a56b85e7f Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Nov 2020 00:54:03 +1100 Subject: [PATCH 15/25] delete file --- .../EnsureUserPermissionForMediaAttribute.cs | 184 ------------------ 1 file changed, 184 deletions(-) delete mode 100644 src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs diff --git a/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs deleted file mode 100644 index 607d77cd21..0000000000 --- a/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs +++ /dev/null @@ -1,184 +0,0 @@ -//using System; -//using Microsoft.AspNetCore.Mvc; -//using Microsoft.AspNetCore.Mvc.Filters; -//using Umbraco.Core; -//using Umbraco.Core.Models; -//using Umbraco.Core.Security; -//using Umbraco.Core.Services; -//using Umbraco.Web.BackOffice.Controllers; -//using Umbraco.Web.Common.Exceptions; -//using Umbraco.Web.Editors; -//using Umbraco.Web.Security; - -//namespace Umbraco.Web.BackOffice.Filters -//{ -// /// -// /// Auth filter to check if the current user has access to the content item -// /// -// /// -// /// Since media doesn't have permissions, this simply checks start node access -// /// -// internal sealed class EnsureUserPermissionForMediaAttribute : TypeFilterAttribute -// { -// // TODO: This needs to be an authorization policy - -// public EnsureUserPermissionForMediaAttribute(int nodeId) -// : base(typeof(EnsureUserPermissionForMediaFilter)) -// { -// Arguments = new object[] -// { -// nodeId -// }; -// } - -// public EnsureUserPermissionForMediaAttribute(string paramName) -// : base(typeof(EnsureUserPermissionForMediaFilter)) -// { -// Arguments = new object[] -// { -// paramName -// }; -// } -// private sealed class EnsureUserPermissionForMediaFilter : IActionFilter -// { -// private readonly int? _nodeId; -// private readonly string _paramName; -// private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; -// private readonly IEntityService _entityService; -// private readonly IMediaService _mediaService; - -// /// -// /// This constructor will only be able to test the start node access -// /// -// public EnsureUserPermissionForMediaFilter( -// IBackOfficeSecurityAccessor backofficeSecurityAccessor, -// IEntityService entityService, -// IMediaService mediaService, -// int nodeId) -// :this(backofficeSecurityAccessor, entityService, mediaService, nodeId, null) -// { -// _nodeId = nodeId; -// } - -// public EnsureUserPermissionForMediaFilter( -// IBackOfficeSecurityAccessor backofficeSecurityAccessor, -// IEntityService entityService, -// IMediaService mediaService, -// string paramName) -// :this(backofficeSecurityAccessor, entityService, mediaService,null, paramName) -// { -// if (paramName == null) throw new ArgumentNullException(nameof(paramName)); -// if (string.IsNullOrEmpty(paramName)) -// throw new ArgumentException("Value can't be empty.", nameof(paramName)); -// } - -// private EnsureUserPermissionForMediaFilter( -// IBackOfficeSecurityAccessor backofficeSecurityAccessor, -// IEntityService entityService, -// IMediaService mediaService, -// int? nodeId, string paramName) -// { -// _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); -// _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); -// _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); - -// _paramName = paramName; - -// if (nodeId.HasValue) -// { -// _nodeId = nodeId.Value; -// } -// } - -// private int GetNodeIdFromParameter(object parameterValue) -// { -// if (parameterValue is int) -// { -// return (int) parameterValue; -// } - -// var guidId = Guid.Empty; -// if (parameterValue is Guid) -// { -// guidId = (Guid) parameterValue; -// } -// else if (parameterValue is GuidUdi) -// { -// guidId = ((GuidUdi) parameterValue).Guid; -// } - -// if (guidId != Guid.Empty) -// { -// var found = _entityService.GetId(guidId, UmbracoObjectTypes.Media); -// if (found) -// return found.Result; -// } - -// throw new InvalidOperationException("The id type: " + parameterValue.GetType() + -// " is not a supported id"); -// } - -// public void OnActionExecuting(ActionExecutingContext context) -// { -// if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser == null) -// { -// throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); -// } - -// int nodeId; -// if (_nodeId.HasValue == false) -// { -// var parts = _paramName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - -// if (context.ActionArguments[parts[0]] == null) -// { -// throw new InvalidOperationException("No argument found for the current action with the name: " + -// _paramName); -// } - -// if (parts.Length == 1) -// { -// nodeId = GetNodeIdFromParameter(context.ActionArguments[parts[0]]); -// } -// else -// { -// //now we need to see if we can get the property of whatever object it is -// var pType = context.ActionArguments[parts[0]].GetType(); -// var prop = pType.GetProperty(parts[1]); -// if (prop == null) -// { -// throw new InvalidOperationException( -// "No argument found for the current action with the name: " + _paramName); -// } - -// nodeId = GetNodeIdFromParameter(prop.GetValue(context.ActionArguments[parts[0]])); -// } -// } -// else -// { -// nodeId = _nodeId.Value; -// } - -// if (MediaController.CheckPermissions( -// context.HttpContext.Items, -// _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, -// _mediaService, -// _entityService, -// nodeId)) -// { - -// } -// else -// { -// throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); -// } -// } - -// public void OnActionExecuted(ActionExecutedContext context) -// { - -// } - -// } -// } -//} From f313a4f583c1c9f5fedac09c018aec51eb8f7fa4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Nov 2020 11:56:53 +1100 Subject: [PATCH 16/25] some cleanup of handlers that check multiple query strings --- .../Authorization/AdminUsersHandler.cs | 62 +++++++++-------- .../ContentPermissionsQueryStringHandler.cs | 66 ++++++++----------- ...ontentPermissionsQueryStringRequirement.cs | 6 +- .../MediaPermissionsQueryStringHandler.cs | 35 ++++------ .../MediaPermissionsQueryStringRequirement.cs | 6 +- .../Controllers/AuthenticationController.cs | 1 - .../BackOfficeServiceCollectionExtensions.cs | 34 +++++----- 7 files changed, 98 insertions(+), 112 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs index 05c18b4bb6..14f76c48dd 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Umbraco.Core; @@ -33,8 +34,40 @@ namespace Umbraco.Web.BackOffice.Authorization protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminUsersRequirement requirement) { - var isAuth = IsAuthorized(requirement); - if (!isAuth.HasValue || isAuth.Value) + int[] userIds; + + var queryString = _httpContextAcessor.HttpContext?.Request.Query[requirement.QueryStringName]; + if (!queryString.HasValue) + { + // don't set status since we cannot determine ourselves + return Task.CompletedTask; + } + + if (int.TryParse(queryString, out var userId)) + { + userIds = new[] { userId }; + } + else + { + var ids = _httpContextAcessor.HttpContext.Request.Query.Where(x => x.Key == requirement.QueryStringName).ToList(); + if (ids.Count == 0) + { + // don't set status since we cannot determine ourselves + return Task.CompletedTask; + } + userIds = ids.Select(x => x.Value.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); + } + + if (userIds.Length == 0) + { + // don't set status since we cannot determine ourselves + return Task.CompletedTask; + } + + var users = _userService.GetUsersById(userIds); + var isAuth = users.All(user => _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, null) != false); + + if (isAuth) { context.Succeed(requirement); } @@ -45,30 +78,5 @@ namespace Umbraco.Web.BackOffice.Authorization return Task.CompletedTask; } - - private bool? IsAuthorized(AdminUsersRequirement requirement) - { - int[] userIds; - - var queryString = _httpContextAcessor.HttpContext?.Request.Query[requirement.QueryStringName]; - if (!queryString.HasValue) return null; - - if (int.TryParse(queryString, out var userId)) - { - userIds = new[] { userId }; - } - else - { - var ids = _httpContextAcessor.HttpContext.Request.Query.Where(x => x.Key == requirement.QueryStringName).ToArray(); - if (ids.Length == 0) - return null; - userIds = ids.Select(x => x.Value.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); - } - - if (userIds.Length == 0) return null; - - var users = _userService.GetUsersById(userIds); - return users.All(user => _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, null) != false); - } } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs index 0534b92fc9..4d9e5baf1d 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs @@ -39,36 +39,30 @@ namespace Umbraco.Web.BackOffice.Authorization int nodeId; if (requirement.NodeId.HasValue == false) { - StringValues routeVal; - foreach(var qs in requirement.QueryStringNames) + if (!_httpContextAccessor.HttpContext.Request.Query.TryGetValue(requirement.QueryStringName, out var routeVal)) { - if (_httpContextAccessor.HttpContext.Request.Query.TryGetValue(qs, out routeVal)) - { - break; - } - } - - if (routeVal.Count == 0) - { - throw new InvalidOperationException("No argument found for the current action with by names " + string.Join(", ", requirement.QueryStringNames)); - } - - var argument = routeVal.ToString(); - // if the argument is an int, it will parse and can be assigned to nodeId - // if might be a udi, so check that next - // otherwise treat it as a guid - unlikely we ever get here - if (int.TryParse(argument, out int parsedId)) - { - nodeId = parsedId; - } - else if (UdiParser.TryParse(argument, true, out var udi)) - { - nodeId = _entityService.GetId(udi).Result; + // don't set status since we cannot determine ourselves + return Task.CompletedTask; } else { - Guid.TryParse(argument, out Guid key); - nodeId = _entityService.GetId(key, UmbracoObjectTypes.Document).Result; + var argument = routeVal.ToString(); + // if the argument is an int, it will parse and can be assigned to nodeId + // if might be a udi, so check that next + // otherwise treat it as a guid - unlikely we ever get here + if (int.TryParse(argument, out int parsedId)) + { + nodeId = parsedId; + } + else if (UdiParser.TryParse(argument, true, out var udi)) + { + nodeId = _entityService.GetId(udi).Result; + } + else + { + Guid.TryParse(argument, out Guid key); + nodeId = _entityService.GetId(key, UmbracoObjectTypes.Document).Result; + } } } else @@ -81,21 +75,17 @@ namespace Umbraco.Web.BackOffice.Authorization out IContent contentItem, new[] { requirement.PermissionToCheck }); - if (permissionResult == ContentPermissions.ContentAccess.NotFound) + switch (permissionResult) { - return null; + case ContentPermissions.ContentAccess.Denied: + context.Fail(); + break; + case ContentPermissions.ContentAccess.NotFound: + default: + context.Succeed(requirement); + break; } - if (permissionResult == ContentPermissions.ContentAccess.Denied) - { - context.Fail(); - } - else - { - context.Succeed(requirement); - } - - if (contentItem != null) { //store the content item in request cache so it can be resolved in the controller without re-looking it up diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs index 9153427614..2d558c569c 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs @@ -25,14 +25,14 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// /// - public ContentPermissionsQueryStringRequirement(char permissionToCheck, string[] paramNames) + public ContentPermissionsQueryStringRequirement(char permissionToCheck, string paramName = "id") { - QueryStringNames = paramNames; + QueryStringName = paramName; PermissionToCheck = permissionToCheck; } public int? NodeId { get; } - public string[] QueryStringNames { get; } + public string QueryStringName { get; } public char PermissionToCheck { get; } } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs index 09983064ad..a4f8d32ee6 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs @@ -20,27 +20,21 @@ namespace Umbraco.Web.BackOffice.Authorization public MediaPermissionsQueryStringHandler( IBackOfficeSecurityAccessor backofficeSecurityAccessor, IHttpContextAccessor httpContextAccessor, + IEntityService entityService, MediaPermissions mediaPermissions) { _backofficeSecurityAccessor = backofficeSecurityAccessor; _httpContextAccessor = httpContextAccessor; + _entityService = entityService; _mediaPermissions = mediaPermissions; } protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MediaPermissionsQueryStringRequirement requirement) { - StringValues routeVal; - foreach (var qs in requirement.QueryStringNames) + if (!_httpContextAccessor.HttpContext.Request.Query.TryGetValue(requirement.QueryStringName, out var routeVal)) { - if (_httpContextAccessor.HttpContext.Request.Query.TryGetValue(qs, out routeVal)) - { - break; - } - } - - if (routeVal.Count == 0) - { - throw new InvalidOperationException("No argument found for the current action with by names " + string.Join(", ", requirement.QueryStringNames)); + // don't set status since we cannot determine ourselves + return Task.CompletedTask; } int nodeId; @@ -68,18 +62,15 @@ namespace Umbraco.Web.BackOffice.Authorization nodeId, out var mediaItem); - if (permissionResult == MediaPermissions.MediaAccess.NotFound) + switch (permissionResult) { - return null; - } - - if (permissionResult == MediaPermissions.MediaAccess.Denied) - { - context.Fail(); - } - else - { - context.Succeed(requirement); + case MediaPermissions.MediaAccess.Denied: + context.Fail(); + break; + case MediaPermissions.MediaAccess.NotFound: + default: + context.Succeed(requirement); + break; } if (mediaItem != null) diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringRequirement.cs index 75bb78cea1..c7b62a570f 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringRequirement.cs @@ -4,11 +4,11 @@ namespace Umbraco.Web.BackOffice.Authorization { public class MediaPermissionsQueryStringRequirement : IAuthorizationRequirement { - public MediaPermissionsQueryStringRequirement(string[] paramNames) + public MediaPermissionsQueryStringRequirement(string paramName) { - QueryStringNames = paramNames; + QueryStringName = paramName; } - public string[] QueryStringNames { get; } + public string QueryStringName { get; } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 1a6f9d1c21..e78395321d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -27,7 +27,6 @@ using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Common.Filters; using Umbraco.Web.Common.Security; -using Umbraco.Web.Editors.Filters; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 2c3cb13f6f..6daf5849b1 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -135,18 +135,12 @@ namespace Umbraco.Extensions services.AddAuthorization(options => { - // these are the query strings we will check for media ids when permission checking - var mediaPermissionQueryStrings = new[] { "id" }; - options.AddPolicy(AuthorizationPolicies.MediaPermissionPathById, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new MediaPermissionsQueryStringRequirement(mediaPermissionQueryStrings)); + policy.Requirements.Add(new MediaPermissionsQueryStringRequirement("id")); }); - // these are the query strings we will check for content ids when permission checking - var contentPermissionQueryStrings = new[] { "id", "contentId" }; - options.AddPolicy(AuthorizationPolicies.ContentPermissionEmptyRecycleBin, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); @@ -156,37 +150,41 @@ namespace Umbraco.Extensions options.AddPolicy(AuthorizationPolicies.ContentPermissionAdministrationById, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter, contentPermissionQueryStrings)); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter)); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter, "contentId")); }); options.AddPolicy(AuthorizationPolicies.ContentPermissionProtectById, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter, contentPermissionQueryStrings)); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter)); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter, "contentId")); }); options.AddPolicy(AuthorizationPolicies.ContentPermissionRollbackById, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter, contentPermissionQueryStrings)); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter)); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter, "contentId")); + }); + + options.AddPolicy(AuthorizationPolicies.ContentPermissionPublishById, policy => + { + policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionPublish.ActionLetter)); }); options.AddPolicy(AuthorizationPolicies.ContentPermissionBrowseById, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionPublish.ActionLetter, contentPermissionQueryStrings)); - }); - - options.AddPolicy(AuthorizationPolicies.ContentPermissionBrowseById, policy => - { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter, contentPermissionQueryStrings)); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter)); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter, "contentId")); }); options.AddPolicy(AuthorizationPolicies.ContentPermissionDeleteById, policy => { policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); - policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter, contentPermissionQueryStrings)); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); }); options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => From 1f048f0599e00c7f18dbd65f9974120e17859383 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Nov 2020 12:02:31 +1100 Subject: [PATCH 17/25] some cleanup of handlers that check multiple query strings --- .../Authorization/AdminUsersHandler.cs | 12 +++++++----- .../ContentPermissionsQueryStringHandler.cs | 3 ++- .../MediaPermissionsQueryStringHandler.cs | 3 ++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs index 14f76c48dd..ff7ba13937 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs @@ -34,15 +34,15 @@ namespace Umbraco.Web.BackOffice.Authorization protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminUsersRequirement requirement) { - int[] userIds; - var queryString = _httpContextAcessor.HttpContext?.Request.Query[requirement.QueryStringName]; if (!queryString.HasValue) { - // don't set status since we cannot determine ourselves + // must succeed this requirement since we cannot process it + context.Succeed(requirement); return Task.CompletedTask; } + int[] userIds; if (int.TryParse(queryString, out var userId)) { userIds = new[] { userId }; @@ -52,7 +52,8 @@ namespace Umbraco.Web.BackOffice.Authorization var ids = _httpContextAcessor.HttpContext.Request.Query.Where(x => x.Key == requirement.QueryStringName).ToList(); if (ids.Count == 0) { - // don't set status since we cannot determine ourselves + // must succeed this requirement since we cannot process it + context.Succeed(requirement); return Task.CompletedTask; } userIds = ids.Select(x => x.Value.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); @@ -60,7 +61,8 @@ namespace Umbraco.Web.BackOffice.Authorization if (userIds.Length == 0) { - // don't set status since we cannot determine ourselves + // must succeed this requirement since we cannot process it + context.Succeed(requirement); return Task.CompletedTask; } diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs index 4d9e5baf1d..bba502e78f 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs @@ -41,7 +41,8 @@ namespace Umbraco.Web.BackOffice.Authorization { if (!_httpContextAccessor.HttpContext.Request.Query.TryGetValue(requirement.QueryStringName, out var routeVal)) { - // don't set status since we cannot determine ourselves + // must succeed this requirement since we cannot process it + context.Succeed(requirement); return Task.CompletedTask; } else diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs index a4f8d32ee6..319d5969fd 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs @@ -33,7 +33,8 @@ namespace Umbraco.Web.BackOffice.Authorization { if (!_httpContextAccessor.HttpContext.Request.Query.TryGetValue(requirement.QueryStringName, out var routeVal)) { - // don't set status since we cannot determine ourselves + // must succeed this requirement since we cannot process it + context.Succeed(requirement); return Task.CompletedTask; } From 8e15e265fd8ce44df4cad61a91d810a624a4b712 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 26 Nov 2020 16:52:03 +1100 Subject: [PATCH 18/25] Adds TODOs, puts back UmbracoRequireHttpsAttribute but in a nicer way, ensures test classes initialize the authz policies with the test scheme --- .../TestServerTest/TestAuthHandler.cs | 4 +- .../UmbracoTestServerTestBase.cs | 5 +- .../Authorization/AdminUsersHandler.cs | 4 +- .../UmbracoAuthorizedApiController.cs | 4 +- .../BackOfficeServiceCollectionExtensions.cs | 102 +++++++++--------- .../Extensions/UmbracoBuilderExtensions.cs | 9 +- .../AppendCurrentEventMessagesAttribute.cs | 1 + .../Filters/UmbracoRequireHttpsAttribute.cs | 27 +++++ .../Controllers/UmbracoApiController.cs | 2 + .../Controllers/UmbracoApiControllerBase.cs | 2 + ...bracoApiControllerTypeCollectionBuilder.cs | 2 + .../Extensions/ServiceCollectionExtensions.cs | 3 + 12 files changed, 107 insertions(+), 58 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Filters/UmbracoRequireHttpsAttribute.cs diff --git a/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs b/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs index 08cd49bd81..2cafaf913f 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs @@ -14,6 +14,8 @@ namespace Umbraco.Tests.Integration.TestServerTest { public class TestAuthHandler : AuthenticationHandler { + public const string TestAuthenticationScheme = "Test"; + private readonly BackOfficeSignInManager _backOfficeSignInManager; private readonly BackOfficeIdentityUser _fakeUser; @@ -32,7 +34,7 @@ namespace Umbraco.Tests.Integration.TestServerTest { var principal = await _backOfficeSignInManager.CreateUserPrincipalAsync(_fakeUser); - var ticket = new AuthenticationTicket(principal, "Test"); + var ticket = new AuthenticationTicket(principal, TestAuthenticationScheme); return AuthenticateResult.Success(ticket); } diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index f6ece372ea..5867e6522c 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -47,7 +47,9 @@ namespace Umbraco.Tests.Integration.TestServerTest // Executes after the standard ConfigureServices method builder.ConfigureTestServices(services => { - services.AddAuthentication("Test").AddScheme("Test", options => { }); + // Add a test auth scheme with a test auth handler to authn and assign the user + services.AddAuthentication(TestAuthHandler.TestAuthenticationScheme) + .AddScheme(TestAuthHandler.TestAuthenticationScheme, options => { }); }); }); @@ -142,6 +144,7 @@ namespace Umbraco.Tests.Integration.TestServerTest .AddRuntimeMinifier() .AddBackOffice() .AddBackOfficeIdentity() + .AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme) .AddPreviewSupport() //.WithMiniProfiler() // we don't want this running in tests .AddMvcAndRazor(mvcBuilding: mvcBuilder => diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs index ff7ba13937..13711b825c 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs @@ -1,16 +1,18 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Umbraco.Core; +using Umbraco.Core.Configuration.Models; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Editors; namespace Umbraco.Web.BackOffice.Authorization { - /// /// if the users being edited is an admin then we must ensure that the current user is also an admin /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs index f1a39e1a76..a807c663d0 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs @@ -16,11 +16,11 @@ namespace Umbraco.Web.BackOffice.Controllers /// is logged in using forms authentication which indicates the seconds remaining /// before their timeout expires. /// - [IsBackOffice] + [IsBackOffice] [UmbracoUserTimeoutFilter] [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [DisableBrowserCache] - [RequireHttps] + [UmbracoRequireHttps] [CheckIfUserTicketDataIsStale] [MiddlewareFilter(typeof(UnhandledExceptionLoggerFilter))] public abstract class UmbracoAuthorizedApiController : UmbracoApiController diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 762e405d62..80c6d81b1b 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -83,7 +83,7 @@ namespace Umbraco.Extensions /// Add authorization handlers and policies /// /// - public static void AddBackOfficeAuthorizationPolicies(this IServiceCollection services) + public static void AddBackOfficeAuthorizationPolicies(this IServiceCollection services, string backOfficeAuthenticationScheme = Constants.Security.BackOfficeAuthenticationType) { // NOTE: Even though we are registering these handlers globally they will only actually execute their logic for // any auth defining a matching requirement and scheme. @@ -100,141 +100,141 @@ namespace Umbraco.Extensions services.AddSingleton(); services.AddSingleton(); - services.AddAuthorization(CreatePolicies); + services.AddAuthorization(o => CreatePolicies(o, backOfficeAuthenticationScheme)); } - private static void CreatePolicies(AuthorizationOptions options) + private static void CreatePolicies(AuthorizationOptions options, string backOfficeAuthenticationScheme) { options.AddPolicy(AuthorizationPolicies.MediaPermissionPathById, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new MediaPermissionsQueryStringRequirement("id")); }); options.AddPolicy(AuthorizationPolicies.ContentPermissionEmptyRecycleBin, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(Constants.System.RecycleBinContent, ActionDelete.ActionLetter)); }); options.AddPolicy(AuthorizationPolicies.ContentPermissionAdministrationById, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter)); policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRights.ActionLetter, "contentId")); }); options.AddPolicy(AuthorizationPolicies.ContentPermissionProtectById, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter)); policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter, "contentId")); }); options.AddPolicy(AuthorizationPolicies.ContentPermissionRollbackById, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter)); policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionRollback.ActionLetter, "contentId")); }); options.AddPolicy(AuthorizationPolicies.ContentPermissionPublishById, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionPublish.ActionLetter)); }); options.AddPolicy(AuthorizationPolicies.ContentPermissionBrowseById, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter)); policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionBrowse.ActionLetter, "contentId")); }); options.AddPolicy(AuthorizationPolicies.ContentPermissionDeleteById, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); }); options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new BackOfficeRequirement()); }); options.AddPolicy(AuthorizationPolicies.BackOfficeAccessWithoutApproval, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new BackOfficeRequirement(false)); }); options.AddPolicy(AuthorizationPolicies.AdminUserEditsRequireAdmin, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new AdminUsersRequirement()); policy.Requirements.Add(new AdminUsersRequirement("userIds")); }); options.AddPolicy(AuthorizationPolicies.UserBelongsToUserGroupInRequest, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new UserGroupRequirement()); policy.Requirements.Add(new UserGroupRequirement("userGroupIds")); }); options.AddPolicy(AuthorizationPolicies.DenyLocalLoginIfConfigured, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new DenyLocalLoginRequirement()); }); options.AddPolicy(AuthorizationPolicies.SectionAccessContent, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement(Constants.Applications.Content)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement(Constants.Applications.Content, Constants.Applications.Media)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessUsers, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement(Constants.Applications.Users)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessForTinyMce, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessMedia, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement(Constants.Applications.Media)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessMembers, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement(Constants.Applications.Members)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessPackages, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement(Constants.Applications.Packages)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessSettings, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement(Constants.Applications.Settings)); }); @@ -242,21 +242,21 @@ namespace Umbraco.Extensions // this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here. options.AddPolicy(AuthorizationPolicies.SectionAccessForContentTree, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessForMediaTree, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users, Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members)); }); options.AddPolicy(AuthorizationPolicies.SectionAccessForMemberTree, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members)); }); @@ -264,7 +264,7 @@ namespace Umbraco.Extensions // Permission is granted to this policy if the user has access to any of these sections: Content, media, settings, developer, members options.AddPolicy(AuthorizationPolicies.SectionAccessForDataTypeReading, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new SectionRequirement( Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings, Constants.Applications.Packages)); @@ -272,139 +272,139 @@ namespace Umbraco.Extensions options.AddPolicy(AuthorizationPolicies.TreeAccessDocuments, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.Content)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessUsers, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.Users)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.PartialViews)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViewMacros, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.PartialViewMacros)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessPackages, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.Packages)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessLogs, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.LogViewer)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.DataTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessTemplates, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.Templates)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.RelationTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.DocumentTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberGroups)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.MediaTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMacros, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.Macros)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessLanguages, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.Languages)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDictionary, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary, Constants.Trees.Dictionary)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary, Constants.Trees.Templates)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.DocumentTypes, Constants.Trees.Content)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.MediaTypes, Constants.Trees.Media)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessMembersOrMemberTypes, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberTypes, Constants.Trees.Members)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessAnySchemaTypes, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)); }); options.AddPolicy(AuthorizationPolicies.TreeAccessAnyContentOrTypes, policy => { - policy.AuthenticationSchemes.Add(Constants.Security.BackOfficeAuthenticationType); + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); policy.Requirements.Add(new TreeRequirement( Constants.Trees.DocumentTypes, Constants.Trees.Content, Constants.Trees.MediaTypes, Constants.Trees.Media, diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs index 2b6eb1d111..7262966615 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs @@ -1,6 +1,7 @@ using System; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualBasic; using Umbraco.Core.Builder; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Security; @@ -54,9 +55,13 @@ namespace Umbraco.Extensions return builder; } - public static IUmbracoBuilder AddBackOfficeAuthorizationPolicies(this IUmbracoBuilder builder) + public static IUmbracoBuilder AddBackOfficeAuthorizationPolicies(this IUmbracoBuilder builder, string backOfficeAuthenticationScheme = Umbraco.Core.Constants.Security.BackOfficeAuthenticationType) { - builder.Services.AddBackOfficeAuthorizationPolicies(); + builder.Services.AddBackOfficeAuthorizationPolicies(backOfficeAuthenticationScheme); + // TODO: See other TODOs in things like UmbracoApiControllerBase ... AFAIK all of this is only used for the back office + // so IMO these controllers and the features auth policies should just be moved to the back office project and then this + // ext method can be removed. + builder.Services.AddUmbracoCommonAuthorizationPolicies(); return builder; } diff --git a/src/Umbraco.Web.BackOffice/Filters/AppendCurrentEventMessagesAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/AppendCurrentEventMessagesAttribute.cs index 082d7c5e52..8d8a3ffa9a 100644 --- a/src/Umbraco.Web.BackOffice/Filters/AppendCurrentEventMessagesAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/AppendCurrentEventMessagesAttribute.cs @@ -7,6 +7,7 @@ using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.BackOffice.Filters { + /// /// Automatically checks if any request is a non-GET and if the /// resulting message is INotificationModel in which case it will append any Event Messages diff --git a/src/Umbraco.Web.BackOffice/Filters/UmbracoRequireHttpsAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/UmbracoRequireHttpsAttribute.cs new file mode 100644 index 0000000000..a027531e06 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/UmbracoRequireHttpsAttribute.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Core.Configuration.Models; + +namespace Umbraco.Web.BackOffice.Filters +{ + /// + /// If Umbraco.Core.UseHttps property in web.config is set to true, this filter will redirect any http access to https. + /// + public class UmbracoRequireHttpsAttribute : RequireHttpsAttribute + { + protected override void HandleNonHttpsRequest(AuthorizationFilterContext filterContext) + { + // just like the base class does, we'll just resolve the required services from the httpcontext. + // we want to re-use their code so we don't have much choice, else we have to do some code tricks, + // this is just easiest. + var optionsAccessor = filterContext.HttpContext.RequestServices.GetRequiredService>(); + if (optionsAccessor.Value.UseHttps) + { + // only continue if this flag is set + base.HandleNonHttpsRequest(filterContext); + } + } + } +} diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs index c53b08d3df..e3250c2983 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs @@ -7,6 +7,8 @@ namespace Umbraco.Web.Common.Controllers /// public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable { + // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK + protected UmbracoApiController() { } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs index 87b7ee7c72..9f9e2b19be 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs @@ -19,6 +19,8 @@ namespace Umbraco.Web.Common.Controllers [UmbracoApiController] public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature { + // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK + public UmbracoApiControllerBase() { } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs index 4bffb0d5ba..8d68e95dd8 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs @@ -5,6 +5,8 @@ namespace Umbraco.Web.Common.Controllers { public class UmbracoApiControllerTypeCollectionBuilder : TypeCollectionBuilderBase { + // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK + protected override UmbracoApiControllerTypeCollectionBuilder This => this; } } diff --git a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs index 0a51ace294..e8e3e2c329 100644 --- a/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs @@ -9,6 +9,9 @@ namespace Umbraco.Extensions { public static void AddUmbracoCommonAuthorizationPolicies(this IServiceCollection services) { + // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK + // If it is moved it should only target the back office scheme + services.AddSingleton(); services.AddAuthorization(options => From 75e6eb0bd910b5ba06fcaabbd419466f77560736 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 26 Nov 2020 17:02:07 +1100 Subject: [PATCH 19/25] Fixes async filters and cleans up some code --- .../Filters/ContentSaveValidationAttribute.cs | 16 ++++++---------- .../Filters/MediaItemSaveValidationAttribute.cs | 6 +++++- .../ValidateAngularAntiForgeryTokenAttribute.cs | 14 +++++++------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index cefa8c7123..29ed0f5ba0 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -33,13 +33,10 @@ namespace Umbraco.Web.BackOffice.Filters private sealed class ContentSaveValidationFilter : IAsyncActionFilter { private readonly IContentService _contentService; - private readonly IEntityService _entityService; private readonly IPropertyValidationService _propertyValidationService; - private readonly ContentPermissions _contentPermissions; private readonly IAuthorizationService _authorizationService; private readonly ILoggerFactory _loggerFactory; private readonly ILocalizedTextService _textService; - private readonly IUserService _userService; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; @@ -48,20 +45,14 @@ namespace Umbraco.Web.BackOffice.Filters IBackOfficeSecurityAccessor backofficeSecurityAccessor, ILocalizedTextService textService, IContentService contentService, - IUserService userService, - IEntityService entityService, IPropertyValidationService propertyValidationService, - ContentPermissions contentPermissions, IAuthorizationService authorizationService) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); _propertyValidationService = propertyValidationService ?? throw new ArgumentNullException(nameof(propertyValidationService)); - _contentPermissions = contentPermissions; _authorizationService = authorizationService; } @@ -70,7 +61,12 @@ namespace Umbraco.Web.BackOffice.Filters // on executing... await OnActionExecutingAsync(context); - await next(); //need to pass the execution to next + if (context.Result == null) + { + //need to pass the execution to next if a result was not set + await next(); + } + // on executed... } diff --git a/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs index b10043c73c..ebf21f345e 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs @@ -52,7 +52,11 @@ namespace Umbraco.Web.BackOffice.Filters // on executing... await OnActionExecutingAsync(context); - await next(); //need to pass the execution to next + if (context.Result == null) + { + //need to pass the execution to next if a result was not set + await next(); + } // on executed... } diff --git a/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs index 556a84e0dc..f7fd174e03 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs @@ -53,13 +53,13 @@ namespace Umbraco.Web.BackOffice.Filters var cookieToken = _cookieManager.GetCookieValue(Constants.Web.CsrfValidationCookieName); var httpContext = context.HttpContext; - var validateResult = await ValidateHeaders(httpContext, cookieToken); - if (validateResult.Item1 == false) - { - httpContext.SetReasonPhrase(validateResult.Item2); - context.Result = new StatusCodeResult((int)HttpStatusCode.ExpectationFailed); - return; - } + var validateResult = await ValidateHeaders(httpContext, cookieToken); + if (validateResult.Item1 == false) + { + httpContext.SetReasonPhrase(validateResult.Item2); + context.Result = new StatusCodeResult((int)HttpStatusCode.ExpectationFailed); + return; + } await next(); } From aeab78f6a1a49d6caa9a69864ba3eb3154c531d9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 26 Nov 2020 17:25:43 +1100 Subject: [PATCH 20/25] reduce code repeat, introduce MustSatisfyRequirementAuthorizationHandler --- .../Authorization/AdminUsersHandler.cs | 25 ++----- .../Authorization/BackOfficeHandler.cs | 24 ++----- .../ContentPermissionsPublishBranchHandler.cs | 15 +--- .../ContentPermissionsQueryStringHandler.cs | 27 +++---- .../ContentPermissionsResourceHandler.cs | 15 +--- .../Authorization/DenyLocalLoginHandler.cs | 15 +--- .../MediaPermissionsQueryStringHandler.cs | 24 +++---- .../MediaPermissionsResourceHandler.cs | 15 +--- ...tSatisfyRequirementAuthorizationHandler.cs | 70 +++++++++++++++++++ .../Authorization/SectionHandler.cs | 20 +----- .../Authorization/TreeHandler.cs | 23 ++---- .../Authorization/UserGroupHandler.cs | 24 ++----- 12 files changed, 127 insertions(+), 170 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Authorization/MustSatisfyRequirementAuthorizationHandler.cs diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs index 13711b825c..3a4e125a28 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs @@ -13,10 +13,11 @@ using Umbraco.Web.Editors; namespace Umbraco.Web.BackOffice.Authorization { + /// /// if the users being edited is an admin then we must ensure that the current user is also an admin /// - public class AdminUsersHandler : AuthorizationHandler + public class AdminUsersHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IHttpContextAccessor _httpContextAcessor; private readonly IUserService _userService; @@ -34,14 +35,13 @@ namespace Umbraco.Web.BackOffice.Authorization _userEditorAuthorizationHelper = userEditorAuthorizationHelper; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminUsersRequirement requirement) + protected override Task IsAuthorized(AuthorizationHandlerContext context, AdminUsersRequirement requirement) { var queryString = _httpContextAcessor.HttpContext?.Request.Query[requirement.QueryStringName]; if (!queryString.HasValue) { // must succeed this requirement since we cannot process it - context.Succeed(requirement); - return Task.CompletedTask; + return Task.FromResult(true); } int[] userIds; @@ -55,8 +55,7 @@ namespace Umbraco.Web.BackOffice.Authorization if (ids.Count == 0) { // must succeed this requirement since we cannot process it - context.Succeed(requirement); - return Task.CompletedTask; + return Task.FromResult(true); } userIds = ids.Select(x => x.Value.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); } @@ -64,23 +63,13 @@ namespace Umbraco.Web.BackOffice.Authorization if (userIds.Length == 0) { // must succeed this requirement since we cannot process it - context.Succeed(requirement); - return Task.CompletedTask; + return Task.FromResult(true); } var users = _userService.GetUsersById(userIds); var isAuth = users.All(user => _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, null) != false); - if (isAuth) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; + return Task.FromResult(isAuth); } } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs index 8c1da3b204..6cee04deae 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs @@ -9,7 +9,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Ensures authorization is successful for a back office user. /// - public class BackOfficeHandler : AuthorizationHandler + public class BackOfficeHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backOfficeSecurity; private readonly IRuntimeState _runtimeState; @@ -20,34 +20,22 @@ namespace Umbraco.Web.BackOffice.Authorization _runtimeState = runtimeState; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BackOfficeRequirement requirement) - { - if (!IsAuthorized(requirement)) - { - context.Fail(); - } - else - { - context.Succeed(requirement); - } - - return Task.CompletedTask; - } - - private bool IsAuthorized(BackOfficeRequirement requirement) + protected override Task IsAuthorized(AuthorizationHandlerContext context, BackOfficeRequirement requirement) { try { // if not configured (install or upgrade) then we can continue // otherwise we need to ensure that a user is logged in - return _runtimeState.Level == RuntimeLevel.Install + var isAuth = _runtimeState.Level == RuntimeLevel.Install || _runtimeState.Level == RuntimeLevel.Upgrade || _backOfficeSecurity.BackOfficeSecurity?.ValidateCurrentUser(false, requirement.RequireApproval) == ValidateRequestAttempt.Success; + return Task.FromResult(isAuth); } catch (Exception) { - return false; + return Task.FromResult(false); } } + } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandler.cs index 89e72f6cba..265e34fe66 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandler.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// The user must have access to all descendant nodes of the content item in order to continue /// - public class ContentPermissionsPublishBranchHandler : AuthorizationHandler + public class ContentPermissionsPublishBranchHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IEntityService _entityService; private readonly ContentPermissions _contentPermissions; @@ -29,7 +29,7 @@ namespace Umbraco.Web.BackOffice.Authorization _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionsPublishBranchRequirement requirement, IContent resource) + protected override Task IsAuthorized(AuthorizationHandlerContext context, ContentPermissionsPublishBranchRequirement requirement, IContent resource) { var denied = new List(); var page = 0; @@ -55,16 +55,7 @@ namespace Umbraco.Web.BackOffice.Authorization } } - if (denied.Count == 0) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; + return Task.FromResult(denied.Count == 0); } } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs index bba502e78f..b947014dbc 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs @@ -15,7 +15,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Used to authorize if the user has the correct permission access to the content for the content id specified in a query string /// - public class ContentPermissionsQueryStringHandler : AuthorizationHandler + public class ContentPermissionsQueryStringHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly IHttpContextAccessor _httpContextAccessor; @@ -34,7 +34,7 @@ namespace Umbraco.Web.BackOffice.Authorization _contentPermissions = contentPermissions; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionsQueryStringRequirement requirement) + protected override Task IsAuthorized(AuthorizationHandlerContext context, ContentPermissionsQueryStringRequirement requirement) { int nodeId; if (requirement.NodeId.HasValue == false) @@ -42,8 +42,7 @@ namespace Umbraco.Web.BackOffice.Authorization if (!_httpContextAccessor.HttpContext.Request.Query.TryGetValue(requirement.QueryStringName, out var routeVal)) { // must succeed this requirement since we cannot process it - context.Succeed(requirement); - return Task.CompletedTask; + return Task.FromResult(true); } else { @@ -72,29 +71,21 @@ namespace Umbraco.Web.BackOffice.Authorization } var permissionResult = _contentPermissions.CheckPermissions(nodeId, - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, out IContent contentItem, new[] { requirement.PermissionToCheck }); - switch (permissionResult) - { - case ContentPermissions.ContentAccess.Denied: - context.Fail(); - break; - case ContentPermissions.ContentAccess.NotFound: - default: - context.Succeed(requirement); - break; - } - if (contentItem != null) { //store the content item in request cache so it can be resolved in the controller without re-looking it up _httpContextAccessor.HttpContext.Items[typeof(IContent).ToString()] = contentItem; } - return Task.CompletedTask; + return permissionResult switch + { + ContentPermissions.ContentAccess.Denied => Task.FromResult(false), + _ => Task.FromResult(true), + }; } - } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs index b2aeecb0ac..fe956c7360 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs @@ -9,7 +9,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Used to authorize if the user has the correct permission access to the content for the specified /// - public class ContentPermissionsResourceHandler : AuthorizationHandler + public class ContentPermissionsResourceHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly ContentPermissions _contentPermissions; @@ -22,22 +22,13 @@ namespace Umbraco.Web.BackOffice.Authorization _contentPermissions = contentPermissions; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ContentPermissionsResourceRequirement requirement, IContent resource) + protected override Task IsAuthorized(AuthorizationHandlerContext context, ContentPermissionsResourceRequirement requirement, IContent resource) { var permissionResult = _contentPermissions.CheckPermissions(resource, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, requirement.PermissionsToCheck); - if (permissionResult == ContentPermissions.ContentAccess.Denied) - { - context.Fail(); - } - else - { - context.Succeed(requirement); - } - - return Task.CompletedTask; + return Task.FromResult(permissionResult != ContentPermissions.ContentAccess.Denied); } } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs index 70656e202f..771f462b8c 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Ensures the resource cannot be accessed if returns true /// - public class DenyLocalLoginHandler : AuthorizationHandler + public class DenyLocalLoginHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IBackOfficeExternalLoginProviders _externalLogins; @@ -17,18 +17,9 @@ namespace Umbraco.Web.BackOffice.Authorization _externalLogins = externalLogins; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyLocalLoginRequirement requirement) + protected override Task IsAuthorized(AuthorizationHandlerContext context, DenyLocalLoginRequirement requirement) { - if (_externalLogins.HasDenyLocalLogin()) - { - context.Fail(); - } - else - { - context.Succeed(requirement); - } - - return Task.CompletedTask; + return Task.FromResult(!_externalLogins.HasDenyLocalLogin()); } } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs index 319d5969fd..a8d3b72918 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Services; namespace Umbraco.Web.BackOffice.Authorization { - public class MediaPermissionsQueryStringHandler : AuthorizationHandler + public class MediaPermissionsQueryStringHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly IHttpContextAccessor _httpContextAccessor; @@ -29,13 +29,12 @@ namespace Umbraco.Web.BackOffice.Authorization _mediaPermissions = mediaPermissions; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MediaPermissionsQueryStringRequirement requirement) + protected override Task IsAuthorized(AuthorizationHandlerContext context, MediaPermissionsQueryStringRequirement requirement) { if (!_httpContextAccessor.HttpContext.Request.Query.TryGetValue(requirement.QueryStringName, out var routeVal)) { // must succeed this requirement since we cannot process it - context.Succeed(requirement); - return Task.CompletedTask; + return Task.FromResult(true); } int nodeId; @@ -63,24 +62,17 @@ namespace Umbraco.Web.BackOffice.Authorization nodeId, out var mediaItem); - switch (permissionResult) - { - case MediaPermissions.MediaAccess.Denied: - context.Fail(); - break; - case MediaPermissions.MediaAccess.NotFound: - default: - context.Succeed(requirement); - break; - } - if (mediaItem != null) { //store the content item in request cache so it can be resolved in the controller without re-looking it up _httpContextAccessor.HttpContext.Items[typeof(IMedia).ToString()] = mediaItem; } - return Task.CompletedTask; + return permissionResult switch + { + MediaPermissions.MediaAccess.Denied => Task.FromResult(false), + _ => Task.FromResult(true), + }; } } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs index 34451ade11..8b016bf466 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Used to authorize if the user has the correct permission access to the content for the specified /// - public class MediaPermissionsResourceHandler : AuthorizationHandler + public class MediaPermissionsResourceHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly MediaPermissions _mediaPermissions; @@ -21,7 +21,7 @@ namespace Umbraco.Web.BackOffice.Authorization _mediaPermissions = mediaPermissions; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MediaPermissionsResourceRequirement requirement, IMedia resource) + protected override Task IsAuthorized(AuthorizationHandlerContext context, MediaPermissionsResourceRequirement requirement, IMedia resource) { var permissionResult = MediaPermissions.MediaAccess.NotFound; @@ -39,16 +39,7 @@ namespace Umbraco.Web.BackOffice.Authorization out _); } - if (permissionResult == MediaPermissions.MediaAccess.Denied) - { - context.Fail(); - } - else - { - context.Succeed(requirement); - } - - return Task.CompletedTask; + return Task.FromResult(permissionResult != MediaPermissions.MediaAccess.Denied); } } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/MustSatisfyRequirementAuthorizationHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MustSatisfyRequirementAuthorizationHandler.cs new file mode 100644 index 0000000000..98baacf5ee --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/MustSatisfyRequirementAuthorizationHandler.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Abstract handler that must satisfy the requirement so Succeed or Fail will be called no matter what + /// + /// + /// + /// aspnetcore Authz handlers are not required to satisfy the requirement and generally don't explicitly call Fail when the requirement + /// isn't satisfied, however in many simple cases explicitly calling Succeed or Fail is what we want which is what this class is used for. + /// + public abstract class MustSatisfyRequirementAuthorizationHandler : AuthorizationHandler where T : IAuthorizationRequirement + { + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement) + { + var isAuth = await IsAuthorized(context, requirement); + if (isAuth) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + } + + /// + /// Return true if the requirement is succeeded or ignored, return false if the requirement is explicitly not met + /// + /// + /// + /// + protected abstract Task IsAuthorized(AuthorizationHandlerContext context, T requirement); + } + + /// + /// Abstract handler that must satisfy the requirement so Succeed or Fail will be called no matter what + /// + /// + /// + /// + /// aspnetcore Authz handlers are not required to satisfy the requirement and generally don't explicitly call Fail when the requirement + /// isn't satisfied, however in many simple cases explicitly calling Succeed or Fail is what we want which is what this class is used for. + /// + public abstract class MustSatisfyRequirementAuthorizationHandler : AuthorizationHandler where T : IAuthorizationRequirement + { + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, T requirement, TResource resource) + { + var isAuth = await IsAuthorized(context, requirement, resource); + if (isAuth) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + } + + /// + /// Return true if the requirement is succeeded or ignored, return false if the requirement is explicitly not met + /// + /// + /// + /// + protected abstract Task IsAuthorized(AuthorizationHandlerContext context, T requirement, TResource resource); + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/SectionHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/SectionHandler.cs index 91ed79c685..ffa6db220f 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/SectionHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/SectionHandler.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// The user only needs access to one of the sections specified, not all of the sections. /// - public class SectionHandler : AuthorizationHandler + public class SectionHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; @@ -21,27 +21,13 @@ namespace Umbraco.Web.BackOffice.Authorization _backofficeSecurityAccessor = backofficeSecurityAccessor; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SectionRequirement requirement) - { - if (IsAuthorized(requirement)) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - - private bool IsAuthorized(SectionRequirement requirement) + protected override Task IsAuthorized(AuthorizationHandlerContext context, SectionRequirement requirement) { var authorized = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null && requirement.SectionAliases.Any(app => _backofficeSecurityAccessor.BackOfficeSecurity.UserHasSectionAccess( app, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser)); - return authorized; + return Task.FromResult(authorized); } } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/TreeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/TreeHandler.cs index 932f467ac4..f151247850 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/TreeHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/TreeHandler.cs @@ -16,7 +16,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// This would allow a tree to be moved between sections. /// The user only needs access to one of the trees specified, not all of the trees. /// - public class TreeHandler : AuthorizationHandler + public class TreeHandler : MustSatisfyRequirementAuthorizationHandler { private readonly ITreeService _treeService; @@ -37,21 +37,7 @@ namespace Umbraco.Web.BackOffice.Authorization _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TreeRequirement requirement) - { - if (IsAuthorized(requirement)) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - - private bool IsAuthorized(TreeRequirement requirement) + protected override Task IsAuthorized(AuthorizationHandlerContext context, TreeRequirement requirement) { var apps = requirement.TreeAliases.Select(x => _treeService .GetByAlias(x)) @@ -60,9 +46,12 @@ namespace Umbraco.Web.BackOffice.Authorization .Distinct() .ToArray(); - return _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null + var isAuth = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null && apps.Any(app => _backofficeSecurityAccessor.BackOfficeSecurity.UserHasSectionAccess( app, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser)); + + return Task.FromResult(isAuth); } + } } diff --git a/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs index 5a9f21df81..697f60ec41 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Authorizes that the current user has access to the user group Id in the request /// - public class UserGroupHandler : AuthorizationHandler + public class UserGroupHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IHttpContextAccessor _httpContextAcessor; private readonly IUserService _userService; @@ -36,22 +36,7 @@ namespace Umbraco.Web.BackOffice.Authorization _backofficeSecurityAccessor = backofficeSecurityAccessor; } - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserGroupRequirement requirement) - { - var isAuth = IsAuthorized(requirement); - if (!isAuth.HasValue || isAuth.Value) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - - private bool? IsAuthorized(UserGroupRequirement requirement) + protected override Task IsAuthorized(AuthorizationHandlerContext context, UserGroupRequirement requirement) { var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; @@ -69,8 +54,11 @@ namespace Umbraco.Web.BackOffice.Authorization _contentService, _mediaService, _entityService); - return authHelper.AuthorizeGroupAccess(currentUser, intIds); + var isAuth = authHelper.AuthorizeGroupAccess(currentUser, intIds); + + return Task.FromResult(isAuth.Success); } + } } From e53ab2e3c11bf82346419cb320450e0ef154393e Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 26 Nov 2020 17:28:12 +1100 Subject: [PATCH 21/25] removes old controller --- .../UmbracoAuthorizedController.cs | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 src/Umbraco.Web.Website/Controllers/UmbracoAuthorizedController.cs diff --git a/src/Umbraco.Web.Website/Controllers/UmbracoAuthorizedController.cs b/src/Umbraco.Web.Website/Controllers/UmbracoAuthorizedController.cs deleted file mode 100644 index ad51d9cb4f..0000000000 --- a/src/Umbraco.Web.Website/Controllers/UmbracoAuthorizedController.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Umbraco.Web.Common.Authorization; -using Umbraco.Web.Common.Filters; - -namespace Umbraco.Web.Mvc -{ - /// - /// Provides a base class for authorized Umbraco controllers. - /// - /// - /// This controller essentially just uses a global UmbracoAuthorizeAttribute, inheritors that require more granular control over the - /// authorization of each method can use this attribute instead of inheriting from this controller. - /// - [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] - [DisableBrowserCache] - public abstract class UmbracoAuthorizedController : UmbracoController - { - // TODO: This controller is not used at all, is there a need for this controller? - - } -} From 76cbd76f32442b095f20bcddec39ca9cfc77407c Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Nov 2020 19:09:14 +1100 Subject: [PATCH 22/25] Fixes content resource authz --- .../Filters/ContentModelValidatorTests.cs | 4 +-- .../ContentPermissionsResource.cs | 34 +++++++++++++++++++ .../ContentPermissionsResourceHandler.cs | 18 +++++++--- .../ContentPermissionsResourceRequirement.cs | 23 ------------- .../Controllers/ContentController.cs | 20 +++++------ .../BackOfficeServiceCollectionExtensions.cs | 6 ++++ .../Filters/ContentModelValidator.cs | 14 ++------ .../Filters/ContentSaveModelValidator.cs | 4 +-- .../Filters/ContentSaveValidationAttribute.cs | 26 +++++++------- .../MediaItemSaveValidationAttribute.cs | 8 +---- .../Filters/MediaSaveModelValidator.cs | 4 +-- .../Filters/MemberSaveModelValidator.cs | 7 ++-- .../Filters/MemberSaveValidationAttribute.cs | 5 +-- .../Authorization/AuthorizationPolicies.cs | 1 + 14 files changed, 88 insertions(+), 86 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResource.cs diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/ContentModelValidatorTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/ContentModelValidatorTests.cs index a62fc26ad7..2960455a70 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/ContentModelValidatorTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/ContentModelValidatorTests.cs @@ -139,12 +139,10 @@ namespace Umbraco.Tests.Integration.Umbraco.Web.Backoffice.Filters var logger = Services.GetRequiredService>(); var backofficeSecurityFactory = Services.GetRequiredService(); backofficeSecurityFactory.EnsureBackOfficeSecurity(); - var backofficeSecurityAccessor = Services.GetRequiredService(); - var localizedTextService = Services.GetRequiredService(); var propertyValidationService = Services.GetRequiredService(); var umbracoMapper = Services.GetRequiredService(); - var validator = new ContentSaveModelValidator(logger, backofficeSecurityAccessor.BackOfficeSecurity, localizedTextService, propertyValidationService); + var validator = new ContentSaveModelValidator(logger, propertyValidationService); var content = ContentBuilder.CreateTextpageContent(_contentType, "test", -1); diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResource.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResource.cs new file mode 100644 index 0000000000..0ec92c7af2 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResource.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// The resource used for the + /// + public class ContentPermissionsResource + { + public ContentPermissionsResource(IContent content, char permissionToCheck) + { + PermissionsToCheck = new List { permissionToCheck }; + Content = content; + } + + public ContentPermissionsResource(IContent content, IReadOnlyList permissionToCheck) + { + Content = content; + PermissionsToCheck = permissionToCheck; + } + + public ContentPermissionsResource(IContent content, int nodeId, IReadOnlyList permissionToCheck) + { + Content = content; + NodeId = nodeId; + PermissionsToCheck = permissionToCheck; + } + + public int? NodeId { get; } + public IReadOnlyList PermissionsToCheck { get; } + public IContent Content { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs index fe956c7360..34d76392cc 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs @@ -9,7 +9,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Used to authorize if the user has the correct permission access to the content for the specified /// - public class ContentPermissionsResourceHandler : MustSatisfyRequirementAuthorizationHandler + public class ContentPermissionsResourceHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly ContentPermissions _contentPermissions; @@ -22,11 +22,19 @@ namespace Umbraco.Web.BackOffice.Authorization _contentPermissions = contentPermissions; } - protected override Task IsAuthorized(AuthorizationHandlerContext context, ContentPermissionsResourceRequirement requirement, IContent resource) + protected override Task IsAuthorized(AuthorizationHandlerContext context, ContentPermissionsResourceRequirement requirement, ContentPermissionsResource resource) { - var permissionResult = _contentPermissions.CheckPermissions(resource, - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, - requirement.PermissionsToCheck); + + var permissionResult = resource.NodeId.HasValue + ? _contentPermissions.CheckPermissions( + resource.NodeId.Value, + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + out IContent _, + resource.PermissionsToCheck) + : _contentPermissions.CheckPermissions( + resource.Content, + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + resource.PermissionsToCheck); return Task.FromResult(permissionResult != ContentPermissions.ContentAccess.Denied); } diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceRequirement.cs index ca29362acf..22b69c93da 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceRequirement.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Authorization; -using System.Collections.Generic; using Umbraco.Web.Actions; namespace Umbraco.Web.BackOffice.Authorization @@ -10,27 +9,5 @@ namespace Umbraco.Web.BackOffice.Authorization /// public class ContentPermissionsResourceRequirement : IAuthorizationRequirement { - /// - /// Create an authorization requirement for a resource - /// - /// - public ContentPermissionsResourceRequirement(char permissionToCheck) - { - PermissionsToCheck = new List { permissionToCheck }; - } - - public ContentPermissionsResourceRequirement(IReadOnlyList permissionToCheck) - { - PermissionsToCheck = permissionToCheck; - } - - public ContentPermissionsResourceRequirement(int nodeId, IReadOnlyList permissionToCheck) - { - NodeId = nodeId; - PermissionsToCheck = permissionToCheck; - } - - public int? NodeId { get; } - public IReadOnlyList PermissionsToCheck { get; } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index e9f6e6b8cb..742838c224 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -172,8 +172,8 @@ namespace Umbraco.Web.BackOffice.Controllers if (content == null) return NotFound(); // Authorize... - var requirement = new ContentPermissionsResourceRequirement(ActionRights.ActionLetter); - var authorizationResult = await _authorizationService.AuthorizeAsync(User, content, requirement); + var resource = new ContentPermissionsResource(content, ActionRights.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, content, AuthorizationPolicies.ContentPermissionByResource); if (!authorizationResult.Succeeded) { return Forbid(); @@ -1601,8 +1601,8 @@ namespace Umbraco.Web.BackOffice.Controllers } // Authorize... - var requirement = new ContentPermissionsResourceRequirement(ActionSort.ActionLetter); - var authorizationResult = await _authorizationService.AuthorizeAsync(User, _contentService.GetById(sorted.ParentId), requirement); + var resource = new ContentPermissionsResource(_contentService.GetById(sorted.ParentId), ActionSort.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, resource, AuthorizationPolicies.ContentPermissionByResource); if (!authorizationResult.Succeeded) { return Forbid(); @@ -1636,8 +1636,8 @@ namespace Umbraco.Web.BackOffice.Controllers public async Task PostMove(MoveOrCopy move) { // Authorize... - var requirement = new ContentPermissionsResourceRequirement(ActionMove.ActionLetter); - var authorizationResult = await _authorizationService.AuthorizeAsync(User, _contentService.GetById(move.ParentId), requirement); + var resource = new ContentPermissionsResource(_contentService.GetById(move.ParentId), ActionMove.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, resource, AuthorizationPolicies.ContentPermissionByResource); if (!authorizationResult.Succeeded) { return Forbid(); @@ -1658,8 +1658,8 @@ namespace Umbraco.Web.BackOffice.Controllers public async Task PostCopy(MoveOrCopy copy) { // Authorize... - var requirement = new ContentPermissionsResourceRequirement(ActionCopy.ActionLetter); - var authorizationResult = await _authorizationService.AuthorizeAsync(User, _contentService.GetById(copy.ParentId), requirement); + var resource = new ContentPermissionsResource(_contentService.GetById(copy.ParentId), ActionCopy.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, resource, AuthorizationPolicies.ContentPermissionByResource); if (!authorizationResult.Succeeded) { return Forbid(); @@ -1688,8 +1688,8 @@ namespace Umbraco.Web.BackOffice.Controllers } // Authorize... - var requirement = new ContentPermissionsResourceRequirement(ActionUnpublish.ActionLetter); - var authorizationResult = await _authorizationService.AuthorizeAsync(User, foundContent, requirement); + var resource = new ContentPermissionsResource(foundContent, ActionUnpublish.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, resource, AuthorizationPolicies.ContentPermissionByResource); if (!authorizationResult.Succeeded) { return Forbid(); diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 80c6d81b1b..3b9eb28881 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -111,6 +111,12 @@ namespace Umbraco.Extensions policy.Requirements.Add(new MediaPermissionsQueryStringRequirement("id")); }); + options.AddPolicy(AuthorizationPolicies.ContentPermissionByResource, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsResourceRequirement()); + }); + options.AddPolicy(AuthorizationPolicies.ContentPermissionEmptyRecycleBin, policy => { policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/ContentModelValidator.cs index 0d7a3a14aa..6d757dc983 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentModelValidator.cs @@ -20,15 +20,12 @@ namespace Umbraco.Web.BackOffice.Filters /// internal abstract class ContentModelValidator { - - protected IBackOfficeSecurity BackOfficeSecurity { get; } public IPropertyValidationService PropertyValidationService { get; } protected ILogger Logger { get; } - protected ContentModelValidator(ILogger logger, IBackOfficeSecurity backofficeSecurity, IPropertyValidationService propertyValidationService) + protected ContentModelValidator(ILogger logger, IPropertyValidationService propertyValidationService) { Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - BackOfficeSecurity = backofficeSecurity ?? throw new ArgumentNullException(nameof(backofficeSecurity)); PropertyValidationService = propertyValidationService ?? throw new ArgumentNullException(nameof(propertyValidationService)); } } @@ -47,17 +44,12 @@ namespace Umbraco.Web.BackOffice.Filters where TPersisted : class, IContentBase where TModelSave: IContentSave where TModelWithProperties : IContentProperties - { - private readonly ILocalizedTextService _textService; - + { protected ContentModelValidator( ILogger logger, - IBackOfficeSecurity backofficeSecurity, - ILocalizedTextService textService, IPropertyValidationService propertyValidationService) - : base(logger, backofficeSecurity, propertyValidationService) + : base(logger, propertyValidationService) { - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); } /// diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveModelValidator.cs index caaee1d9e0..b83462fa10 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveModelValidator.cs @@ -13,10 +13,8 @@ namespace Umbraco.Web.BackOffice.Filters { public ContentSaveModelValidator( ILogger logger, - IBackOfficeSecurity backofficeSecurity, - ILocalizedTextService textService, IPropertyValidationService propertyValidationService) - : base(logger, backofficeSecurity, textService, propertyValidationService) + : base(logger, propertyValidationService) { } diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index 29ed0f5ba0..686023a478 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Security; @@ -36,21 +37,15 @@ namespace Umbraco.Web.BackOffice.Filters private readonly IPropertyValidationService _propertyValidationService; private readonly IAuthorizationService _authorizationService; private readonly ILoggerFactory _loggerFactory; - private readonly ILocalizedTextService _textService; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; public ContentSaveValidationFilter( ILoggerFactory loggerFactory, - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - ILocalizedTextService textService, IContentService contentService, IPropertyValidationService propertyValidationService, IAuthorizationService authorizationService) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); _propertyValidationService = propertyValidationService ?? throw new ArgumentNullException(nameof(propertyValidationService)); _authorizationService = authorizationService; @@ -74,11 +69,11 @@ namespace Umbraco.Web.BackOffice.Filters private async Task OnActionExecutingAsync(ActionExecutingContext context) { var model = (ContentItemSave) context.ActionArguments["contentItem"]; - var contentItemValidator = new ContentSaveModelValidator(_loggerFactory.CreateLogger(), _backofficeSecurityAccessor.BackOfficeSecurity, _textService, _propertyValidationService); + var contentItemValidator = new ContentSaveModelValidator(_loggerFactory.CreateLogger(), _propertyValidationService); if (!ValidateAtLeastOneVariantIsBeingSaved(model, context)) return; if (!contentItemValidator.ValidateExistingContent(model, context)) return; - if (!await ValidateUserAccessAsync(model, context, _backofficeSecurityAccessor.BackOfficeSecurity)) return; + if (!await ValidateUserAccessAsync(model, context)) return; //validate for each variant that is being updated foreach (var variant in model.Variants.Where(x => x.Save)) @@ -117,8 +112,7 @@ namespace Umbraco.Web.BackOffice.Filters /// private async Task ValidateUserAccessAsync( ContentItemSave contentItem, - ActionExecutingContext actionContext, - IBackOfficeSecurity backofficeSecurity) + ActionExecutingContext actionContext) { // We now need to validate that the user is allowed to be doing what they are doing. // Based on the action we need to check different permissions. @@ -226,11 +220,15 @@ namespace Umbraco.Web.BackOffice.Filters } - var requirement = contentToCheck == null - ? new ContentPermissionsResourceRequirement(contentIdToCheck, permissionToCheck) - : new ContentPermissionsResourceRequirement(permissionToCheck); + var resource = contentToCheck == null + ? new ContentPermissionsResource(contentToCheck, contentIdToCheck, permissionToCheck) + : new ContentPermissionsResource(contentToCheck, permissionToCheck); + + var authorizationResult = await _authorizationService.AuthorizeAsync( + actionContext.HttpContext.User, + resource, + AuthorizationPolicies.ContentPermissionByResource); - var authorizationResult = await _authorizationService.AuthorizeAsync(actionContext.HttpContext.User, contentToCheck, requirement); if (!authorizationResult.Succeeded) { actionContext.Result = new ForbidResult(); diff --git a/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs index ebf21f345e..52ec08e4b4 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs @@ -27,21 +27,15 @@ namespace Umbraco.Web.BackOffice.Filters private readonly IPropertyValidationService _propertyValidationService; private readonly IAuthorizationService _authorizationService; private readonly IMediaService _mediaService; - private readonly ILocalizedTextService _textService; private readonly ILoggerFactory _loggerFactory; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; public MediaItemSaveValidationFilter( ILoggerFactory loggerFactory, - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - ILocalizedTextService textService, IMediaService mediaService, IPropertyValidationService propertyValidationService, IAuthorizationService authorizationService) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); _propertyValidationService = propertyValidationService ?? throw new ArgumentNullException(nameof(propertyValidationService)); _authorizationService = authorizationService ?? throw new ArgumentNullException(nameof(authorizationService)); @@ -64,7 +58,7 @@ namespace Umbraco.Web.BackOffice.Filters private async Task OnActionExecutingAsync(ActionExecutingContext context) { var model = (MediaItemSave) context.ActionArguments["contentItem"]; - var contentItemValidator = new MediaSaveModelValidator(_loggerFactory.CreateLogger(), _backofficeSecurityAccessor.BackOfficeSecurity, _textService, _propertyValidationService); + var contentItemValidator = new MediaSaveModelValidator(_loggerFactory.CreateLogger(), _propertyValidationService); if (await ValidateUserAccessAsync(model, context)) { diff --git a/src/Umbraco.Web.BackOffice/Filters/MediaSaveModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/MediaSaveModelValidator.cs index b398a4e401..0a59aadfa6 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MediaSaveModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MediaSaveModelValidator.cs @@ -13,10 +13,8 @@ namespace Umbraco.Web.BackOffice.Filters { public MediaSaveModelValidator( ILogger logger, - IBackOfficeSecurity backofficeSecurity, - ILocalizedTextService textService, IPropertyValidationService propertyValidationService) - : base(logger, backofficeSecurity, textService, propertyValidationService) + : base(logger, propertyValidationService) { } } diff --git a/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs index 275220c8b4..65056e1a5b 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs @@ -20,6 +20,7 @@ namespace Umbraco.Web.BackOffice.Filters /// internal class MemberSaveModelValidator : ContentModelValidator> { + private readonly IBackOfficeSecurity _backofficeSecurity; private readonly IMemberTypeService _memberTypeService; private readonly IMemberService _memberService; private readonly IShortStringHelper _shortStringHelper; @@ -27,13 +28,13 @@ namespace Umbraco.Web.BackOffice.Filters public MemberSaveModelValidator( ILogger logger, IBackOfficeSecurity backofficeSecurity, - ILocalizedTextService textService, IMemberTypeService memberTypeService, IMemberService memberService, IShortStringHelper shortStringHelper, IPropertyValidationService propertyValidationService) - : base(logger, backofficeSecurity, textService, propertyValidationService) + : base(logger, propertyValidationService) { + _backofficeSecurity = backofficeSecurity; _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); @@ -96,7 +97,7 @@ namespace Umbraco.Web.BackOffice.Filters //if the user doesn't have access to sensitive values, then we need to validate the incoming properties to check //if a sensitive value is being submitted. - if (BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData() == false) + if (_backofficeSecurity.CurrentUser.HasAccessToSensitiveData() == false) { var contentType = _memberTypeService.Get(model.PersistedContent.ContentTypeId); var sensitiveProperties = contentType diff --git a/src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs index 7ba86f525e..b8109b0e0c 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs @@ -24,7 +24,6 @@ namespace Umbraco.Web.BackOffice.Filters { private readonly ILoggerFactory _loggerFactory; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; - private readonly ILocalizedTextService _textService; private readonly IMemberTypeService _memberTypeService; private readonly IMemberService _memberService; private readonly IShortStringHelper _shortStringHelper; @@ -33,7 +32,6 @@ namespace Umbraco.Web.BackOffice.Filters public MemberSaveValidationFilter( ILoggerFactory loggerFactory, IBackOfficeSecurityAccessor backofficeSecurityAccessor, - ILocalizedTextService textService, IMemberTypeService memberTypeService, IMemberService memberService, IShortStringHelper shortStringHelper, @@ -41,7 +39,6 @@ namespace Umbraco.Web.BackOffice.Filters { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); @@ -51,7 +48,7 @@ namespace Umbraco.Web.BackOffice.Filters public void OnActionExecuting(ActionExecutingContext context) { var model = (MemberSave)context.ActionArguments["contentItem"]; - var contentItemValidator = new MemberSaveModelValidator(_loggerFactory.CreateLogger(), _backofficeSecurityAccessor.BackOfficeSecurity, _textService, _memberTypeService, _memberService, _shortStringHelper, _propertyValidationService); + var contentItemValidator = new MemberSaveModelValidator(_loggerFactory.CreateLogger(), _backofficeSecurityAccessor.BackOfficeSecurity, _memberTypeService, _memberService, _shortStringHelper, _propertyValidationService); //now do each validation step if (contentItemValidator.ValidateExistingContent(model, context)) if (contentItemValidator.ValidateProperties(model, model, context)) diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index 335dc5397b..2227912a7e 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -15,6 +15,7 @@ // Content permission access + public const string ContentPermissionByResource = nameof(ContentPermissionByResource); public const string ContentPermissionEmptyRecycleBin = nameof(ContentPermissionEmptyRecycleBin); public const string ContentPermissionAdministrationById = nameof(ContentPermissionAdministrationById); public const string ContentPermissionPublishById = nameof(ContentPermissionPublishById); From 36b3a2b62a90bec2b98b276f4d7d6596b527fcfa Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Nov 2020 19:38:55 +1100 Subject: [PATCH 23/25] fixes media authz resource policies --- .../ContentPermissionsResourceHandler.cs | 1 - .../Authorization/MediaPermissionsResource.cs | 20 +++++++++++++++ .../MediaPermissionsResourceHandler.cs | 25 +++++++------------ .../MediaPermissionsResourceRequirement.cs | 12 ++------- .../BackOfficeServiceCollectionExtensions.cs | 6 +++++ .../MediaItemSaveValidationAttribute.cs | 13 +++++++--- .../Authorization/AuthorizationPolicies.cs | 2 ++ 7 files changed, 48 insertions(+), 31 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResource.cs diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs index 34d76392cc..bcd8ef9add 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs @@ -24,7 +24,6 @@ namespace Umbraco.Web.BackOffice.Authorization protected override Task IsAuthorized(AuthorizationHandlerContext context, ContentPermissionsResourceRequirement requirement, ContentPermissionsResource resource) { - var permissionResult = resource.NodeId.HasValue ? _contentPermissions.CheckPermissions( resource.NodeId.Value, diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResource.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResource.cs new file mode 100644 index 0000000000..5b1ed92f5f --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResource.cs @@ -0,0 +1,20 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Web.BackOffice.Authorization +{ + public class MediaPermissionsResource + { + public MediaPermissionsResource(IMedia media) + { + Media = media; + } + + public MediaPermissionsResource(int nodeId) + { + NodeId = nodeId; + } + + public int? NodeId { get; } + public IMedia Media { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs index 8b016bf466..6c5280a19c 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.BackOffice.Authorization /// /// Used to authorize if the user has the correct permission access to the content for the specified /// - public class MediaPermissionsResourceHandler : MustSatisfyRequirementAuthorizationHandler + public class MediaPermissionsResourceHandler : MustSatisfyRequirementAuthorizationHandler { private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly MediaPermissions _mediaPermissions; @@ -21,23 +21,16 @@ namespace Umbraco.Web.BackOffice.Authorization _mediaPermissions = mediaPermissions; } - protected override Task IsAuthorized(AuthorizationHandlerContext context, MediaPermissionsResourceRequirement requirement, IMedia resource) + protected override Task IsAuthorized(AuthorizationHandlerContext context, MediaPermissionsResourceRequirement requirement, MediaPermissionsResource resource) { - var permissionResult = MediaPermissions.MediaAccess.NotFound; - - if (resource != null) - { - permissionResult = _mediaPermissions.CheckPermissions( - resource, - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); - } - else if (requirement.NodeId.HasValue) - { - permissionResult = _mediaPermissions.CheckPermissions( + var permissionResult = resource.NodeId.HasValue + ? _mediaPermissions.CheckPermissions( _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, - requirement.NodeId.Value, - out _); - } + resource.NodeId.Value, + out _) + : _mediaPermissions.CheckPermissions( + resource.Media, + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); return Task.FromResult(permissionResult != MediaPermissions.MediaAccess.Denied); } diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceRequirement.cs index 0cd51c9c15..3087e4b258 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceRequirement.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceRequirement.cs @@ -2,20 +2,12 @@ namespace Umbraco.Web.BackOffice.Authorization { + /// /// An authorization requirement for /// public class MediaPermissionsResourceRequirement : IAuthorizationRequirement { - public MediaPermissionsResourceRequirement() - { - } - - public MediaPermissionsResourceRequirement(int nodeId) - { - NodeId = nodeId; - } - - public int? NodeId { get; } + } } diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs index 3b9eb28881..d13a908034 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs @@ -105,6 +105,12 @@ namespace Umbraco.Extensions private static void CreatePolicies(AuthorizationOptions options, string backOfficeAuthenticationScheme) { + options.AddPolicy(AuthorizationPolicies.MediaPermissionByResource, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new MediaPermissionsResourceRequirement()); + }); + options.AddPolicy(AuthorizationPolicies.MediaPermissionPathById, policy => { policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); diff --git a/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs index 52ec08e4b4..3ba2b408ef 100644 --- a/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/MediaItemSaveValidationAttribute.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.BackOffice.Authorization; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.BackOffice.Filters @@ -107,11 +108,15 @@ namespace Umbraco.Web.BackOffice.Filters return false; } - var requirement = contentToCheck == null - ? new MediaPermissionsResourceRequirement(contentIdToCheck) - : new MediaPermissionsResourceRequirement(); + var resource = contentToCheck == null + ? new MediaPermissionsResource(contentIdToCheck) + : new MediaPermissionsResource(contentToCheck); + + var authorizationResult = await _authorizationService.AuthorizeAsync( + actionContext.HttpContext.User, + resource, + AuthorizationPolicies.MediaPermissionByResource); - var authorizationResult = await _authorizationService.AuthorizeAsync(actionContext.HttpContext.User, contentToCheck, requirement); if (!authorizationResult.Succeeded) { actionContext.Result = new ForbidResult(); diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index 2227912a7e..56070f5033 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -24,7 +24,9 @@ public const string ContentPermissionBrowseById = nameof(ContentPermissionBrowseById); public const string ContentPermissionDeleteById = nameof(ContentPermissionDeleteById); + public const string MediaPermissionByResource = nameof(MediaPermissionByResource); public const string MediaPermissionPathById = nameof(MediaPermissionPathById); + // Single section access From e431c82edc91d71fbeb866a21672ae0649690112 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 1 Dec 2020 15:20:34 +1100 Subject: [PATCH 24/25] fix returning null issue --- .../Authorization/UserGroupHandler.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs index 697f60ec41..f5c6e0ea2d 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs @@ -42,11 +42,17 @@ namespace Umbraco.Web.BackOffice.Authorization var queryString = _httpContextAcessor.HttpContext?.Request.Query; if (queryString == null) - return null; + { + // must succeed this requirement since we cannot process it + return Task.FromResult(true); + } var ids = queryString.Where(x => x.Key == requirement.QueryStringName).ToArray(); if (ids.Length == 0) - return null; + { + // must succeed this requirement since we cannot process it + return Task.FromResult(true); + } var intIds = ids.Select(x => x.Value.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); var authHelper = new UserGroupEditorAuthorizationHelper( From a995cdc360479b12f331a297623cd4892ffb4a43 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 1 Dec 2020 16:08:16 +1100 Subject: [PATCH 25/25] Fixes TryConvertTo with StringValues --- .../Authorization/AdminUsersHandler.cs | 4 +++- src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs index 3a4e125a28..b40904f460 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs @@ -57,7 +57,9 @@ namespace Umbraco.Web.BackOffice.Authorization // must succeed this requirement since we cannot process it return Task.FromResult(true); } - userIds = ids.Select(x => x.Value.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); + userIds = ids + .Select(x => x.Value.ToString()) + .Select(x => x.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); } if (userIds.Length == 0) diff --git a/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs index f5c6e0ea2d..fc1db9699b 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs @@ -54,7 +54,10 @@ namespace Umbraco.Web.BackOffice.Authorization return Task.FromResult(true); } - var intIds = ids.Select(x => x.Value.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); + var intIds = ids + .Select(x => x.Value.ToString()) + .Select(x => x.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); + var authHelper = new UserGroupEditorAuthorizationHelper( _userService, _contentService,