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); } } }