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.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 63b5cc90dc..d8a5f675b2 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; } @@ -114,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"); } @@ -135,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.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index b710594689..a4a2417602 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -63,6 +63,7 @@ using Umbraco.Core.Builder; using Umbraco.Core.Configuration.HealthChecks; using Umbraco.Core.HealthCheck; using Umbraco.Core.HealthCheck.Checks; +using Umbraco.Core.Security; namespace Umbraco.Core.Runtime { @@ -376,6 +377,11 @@ namespace Umbraco.Core.Runtime builder.Services.AddUnique(); builder.Services.AddUnique(); + + builder.Services.AddUnique(factory => new LegacyPasswordSecurity()); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); } } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs index 3adbc0df2c..0b67498f01 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; @@ -9,6 +10,7 @@ using Umbraco.Core.Hosting; using Umbraco.ModelsBuilder.Embedded.Building; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.Authorization; namespace Umbraco.ModelsBuilder.Embedded.BackOffice { @@ -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.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.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index db71603089..b61e61f20c 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -199,7 +199,7 @@ namespace Umbraco.Tests.Integration.Testing builder.AddWebComponents(); builder.AddRuntimeMinifier(); builder.AddBackOffice(); - services.AddUmbracoBackOfficeIdentity(); + builder.AddBackOfficeIdentity(); services.AddMvc(); 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.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.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/AdminUsersHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs new file mode 100644 index 0000000000..b40904f460 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersHandler.cs @@ -0,0 +1,77 @@ +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 + /// + public class AdminUsersHandler : MustSatisfyRequirementAuthorizationHandler + { + private readonly IHttpContextAccessor _httpContextAcessor; + private readonly IUserService _userService; + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly UserEditorAuthorizationHelper _userEditorAuthorizationHelper; + + public AdminUsersHandler(IHttpContextAccessor httpContextAcessor, + IUserService userService, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + UserEditorAuthorizationHelper userEditorAuthorizationHelper) + { + _httpContextAcessor = httpContextAcessor; + _userService = userService; + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _userEditorAuthorizationHelper = userEditorAuthorizationHelper; + } + + 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 + return Task.FromResult(true); + } + + int[] userIds; + 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) + { + // must succeed this requirement since we cannot process it + return Task.FromResult(true); + } + userIds = ids + .Select(x => x.Value.ToString()) + .Select(x => x.TryConvertTo()).Where(x => x.Success).Select(x => x.Result).ToArray(); + } + + if (userIds.Length == 0) + { + // must succeed this requirement since we cannot process it + return Task.FromResult(true); + } + + var users = _userService.GetUsersById(userIds); + var isAuth = users.All(user => _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, null) != false); + + return Task.FromResult(isAuth); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/AdminUsersRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersRequirement.cs new file mode 100644 index 0000000000..3d168776e9 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/AdminUsersRequirement.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + + /// + /// Authorization requirement for the + /// + public class AdminUsersRequirement : IAuthorizationRequirement + { + public AdminUsersRequirement(string queryStringName = "id") + { + QueryStringName = queryStringName; + } + + public string QueryStringName { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs new file mode 100644 index 0000000000..6cee04deae --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs @@ -0,0 +1,41 @@ +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 BackOfficeHandler : MustSatisfyRequirementAuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backOfficeSecurity; + private readonly IRuntimeState _runtimeState; + + public BackOfficeHandler(IBackOfficeSecurityAccessor backOfficeSecurity, IRuntimeState runtimeState) + { + _backOfficeSecurity = backOfficeSecurity; + _runtimeState = runtimeState; + } + + 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 + 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 Task.FromResult(false); + } + } + + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeRequirement.cs new file mode 100644 index 0000000000..d1b13efe2c --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeRequirement.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Authorization requirement for the + /// + public class BackOfficeRequirement : IAuthorizationRequirement + { + public BackOfficeRequirement(bool requireApproval = true) + { + RequireApproval = requireApproval; + } + + public bool RequireApproval { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandler.cs new file mode 100644 index 0000000000..265e34fe66 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchHandler.cs @@ -0,0 +1,61 @@ +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 ContentPermissionsPublishBranchHandler : MustSatisfyRequirementAuthorizationHandler + { + private readonly IEntityService _entityService; + private readonly ContentPermissions _contentPermissions; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public ContentPermissionsPublishBranchHandler( + IEntityService entityService, + ContentPermissions contentPermissions, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _entityService = entityService; + _contentPermissions = contentPermissions; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + protected override Task IsAuthorized(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); + } + } + } + + return Task.FromResult(denied.Count == 0); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsPublishBranchRequirement.cs new file mode 100644 index 0000000000..541f861f0d --- /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/ContentPermissionsQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs new file mode 100644 index 0000000000..b947014dbc --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs @@ -0,0 +1,91 @@ +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 content id specified in a query string + /// + public class ContentPermissionsQueryStringHandler : MustSatisfyRequirementAuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IEntityService _entityService; + private readonly ContentPermissions _contentPermissions; + + public ContentPermissionsQueryStringHandler( + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IHttpContextAccessor httpContextAccessor, + IEntityService entityService, + ContentPermissions contentPermissions) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _httpContextAccessor = httpContextAccessor; + _entityService = entityService; + _contentPermissions = contentPermissions; + } + + protected override Task IsAuthorized(AuthorizationHandlerContext context, ContentPermissionsQueryStringRequirement requirement) + { + int nodeId; + if (requirement.NodeId.HasValue == false) + { + if (!_httpContextAccessor.HttpContext.Request.Query.TryGetValue(requirement.QueryStringName, out var routeVal)) + { + // must succeed this requirement since we cannot process it + return Task.FromResult(true); + } + else + { + 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 = _contentPermissions.CheckPermissions(nodeId, + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + out IContent contentItem, + new[] { requirement.PermissionToCheck }); + + 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 permissionResult switch + { + ContentPermissions.ContentAccess.Denied => Task.FromResult(false), + _ => Task.FromResult(true), + }; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringRequirement.cs new file mode 100644 index 0000000000..2d558c569c --- /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 paramName = "id") + { + QueryStringName = paramName; + PermissionToCheck = permissionToCheck; + } + + public int? NodeId { get; } + public string QueryStringName { get; } + public char PermissionToCheck { get; } + } +} 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 new file mode 100644 index 0000000000..bcd8ef9add --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceHandler.cs @@ -0,0 +1,41 @@ +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 ContentPermissionsResourceHandler : MustSatisfyRequirementAuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly ContentPermissions _contentPermissions; + + public ContentPermissionsResourceHandler( + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + ContentPermissions contentPermissions) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _contentPermissions = contentPermissions; + } + + protected override Task IsAuthorized(AuthorizationHandlerContext context, ContentPermissionsResourceRequirement requirement, ContentPermissionsResource resource) + { + 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 new file mode 100644 index 0000000000..22b69c93da --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsResourceRequirement.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Actions; + +namespace Umbraco.Web.BackOffice.Authorization +{ + + /// + /// An authorization requirement for + /// + public class ContentPermissionsResourceRequirement : IAuthorizationRequirement + { + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs new file mode 100644 index 0000000000..771f462b8c --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs @@ -0,0 +1,25 @@ +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 DenyLocalLoginHandler : MustSatisfyRequirementAuthorizationHandler + { + private readonly IBackOfficeExternalLoginProviders _externalLogins; + + public DenyLocalLoginHandler(IBackOfficeExternalLoginProviders externalLogins) + { + _externalLogins = externalLogins; + } + + protected override Task IsAuthorized(AuthorizationHandlerContext context, DenyLocalLoginRequirement requirement) + { + return Task.FromResult(!_externalLogins.HasDenyLocalLogin()); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginRequirement.cs new file mode 100644 index 0000000000..a4f3a7e306 --- /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/Authorization/MediaPermissionsQueryStringHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs new file mode 100644 index 0000000000..a8d3b72918 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs @@ -0,0 +1,78 @@ +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 MediaPermissionsQueryStringHandler : MustSatisfyRequirementAuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly MediaPermissions _mediaPermissions; + private readonly IEntityService _entityService; + + public MediaPermissionsQueryStringHandler( + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IHttpContextAccessor httpContextAccessor, + IEntityService entityService, + MediaPermissions mediaPermissions) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _httpContextAccessor = httpContextAccessor; + _entityService = entityService; + _mediaPermissions = mediaPermissions; + } + + 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 + return Task.FromResult(true); + } + + 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 (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 permissionResult switch + { + MediaPermissions.MediaAccess.Denied => Task.FromResult(false), + _ => Task.FromResult(true), + }; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringRequirement.cs new file mode 100644 index 0000000000..c7b62a570f --- /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 paramName) + { + QueryStringName = paramName; + } + + public string QueryStringName { get; } + } +} 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 new file mode 100644 index 0000000000..6c5280a19c --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandler.cs @@ -0,0 +1,38 @@ +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 MediaPermissionsResourceHandler : MustSatisfyRequirementAuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly MediaPermissions _mediaPermissions; + + public MediaPermissionsResourceHandler( + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + MediaPermissions mediaPermissions) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _mediaPermissions = mediaPermissions; + } + + protected override Task IsAuthorized(AuthorizationHandlerContext context, MediaPermissionsResourceRequirement requirement, MediaPermissionsResource resource) + { + var permissionResult = resource.NodeId.HasValue + ? _mediaPermissions.CheckPermissions( + _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, + 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 new file mode 100644 index 0000000000..3087e4b258 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceRequirement.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + + /// + /// An authorization requirement for + /// + public class MediaPermissionsResourceRequirement : IAuthorizationRequirement + { + + } +} 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 new file mode 100644 index 0000000000..ffa6db220f --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/SectionHandler.cs @@ -0,0 +1,33 @@ +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 SectionHandler : MustSatisfyRequirementAuthorizationHandler + { + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + + public SectionHandler(IBackOfficeSecurityAccessor backofficeSecurityAccessor) + { + _backofficeSecurityAccessor = backofficeSecurityAccessor; + } + + 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 Task.FromResult(authorized); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/SectionRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/SectionRequirement.cs new file mode 100644 index 0000000000..033eccdd18 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/SectionRequirement.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authorization; +using System.Collections.Generic; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Authorization requirements for + /// + public class SectionRequirement : IAuthorizationRequirement + { + /// + /// The aliases for sections that the user will need access to + /// + public IReadOnlyCollection SectionAliases { get; } + + public SectionRequirement(params string[] aliases) => SectionAliases = aliases; + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/TreeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/TreeHandler.cs new file mode 100644 index 0000000000..f151247850 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/TreeHandler.cs @@ -0,0 +1,57 @@ +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 section 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 TreeHandler : MustSatisfyRequirementAuthorizationHandler + { + + 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 TreeHandler(ITreeService treeService, IBackOfficeSecurityAccessor backofficeSecurityAccessor) + { + _treeService = treeService ?? throw new ArgumentNullException(nameof(treeService)); + _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); + } + + protected override Task IsAuthorized(AuthorizationHandlerContext context, TreeRequirement requirement) + { + var apps = requirement.TreeAliases.Select(x => _treeService + .GetByAlias(x)) + .WhereNotNull() + .Select(x => x.SectionAlias) + .Distinct() + .ToArray(); + + 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/TreeRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/TreeRequirement.cs new file mode 100644 index 0000000000..7261cee51b --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/TreeRequirement.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Authorization; +using System.Collections.Generic; + +namespace Umbraco.Web.BackOffice.Authorization +{ + + /// + /// Authorization requirements for + /// + public class TreeRequirement : IAuthorizationRequirement + { + /// + /// The aliases for trees that the user will need access to + /// + public IReadOnlyCollection TreeAliases { get; } + + public TreeRequirement(params string[] aliases) => TreeAliases = aliases; + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.cs new file mode 100644 index 0000000000..fc1db9699b --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/UserGroupHandler.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.BackOffice.Controllers; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Authorizes that the current user has access to the user group Id in the request + /// + public class UserGroupHandler : MustSatisfyRequirementAuthorizationHandler + { + 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 UserGroupHandler(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 IsAuthorized(AuthorizationHandlerContext context, UserGroupRequirement requirement) + { + var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; + + var queryString = _httpContextAcessor.HttpContext?.Request.Query; + if (queryString == 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) + { + // must succeed this requirement since we cannot process it + return Task.FromResult(true); + } + + 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, + _mediaService, + _entityService); + + var isAuth = authHelper.AuthorizeGroupAccess(currentUser, intIds); + + return Task.FromResult(isAuth.Success); + } + + } +} diff --git a/src/Umbraco.Web.BackOffice/Authorization/UserGroupRequirement.cs b/src/Umbraco.Web.BackOffice/Authorization/UserGroupRequirement.cs new file mode 100644 index 0000000000..aae5733d96 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Authorization/UserGroupRequirement.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Umbraco.Web.BackOffice.Authorization +{ + /// + /// Authorization requirement for the + /// + public class UserGroupRequirement : IAuthorizationRequirement + { + public UserGroupRequirement(string queryStringName = "id") + { + QueryStringName = queryStringName; + } + + public string QueryStringName { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 1b414279a4..e78395321d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -27,10 +27,11 @@ 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; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -109,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); @@ -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)) @@ -156,7 +157,7 @@ namespace Umbraco.Web.BackOffice.Controllers return _umbracoMapper.Map(user); } - [UmbracoBackOfficeAuthorize] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [ValidateAngularAntiForgeryToken] public async Task PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel) { @@ -241,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() @@ -263,9 +264,9 @@ 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] - [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/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/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index f7508e920f..da9648b7ad 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; @@ -19,6 +20,7 @@ using Umbraco.Extensions; 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; @@ -30,7 +32,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 IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index ad51ea7dfd..742838c224 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -37,19 +37,18 @@ 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.Common.Authorization; +using Umbraco.Web.BackOffice.Authorization; +using System.Threading.Tasks; 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; @@ -71,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; @@ -100,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; @@ -122,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)); @@ -133,7 +135,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(); @@ -160,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 resource = new ContentPermissionsResource(content, ActionRights.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, content, AuthorizationPolicies.ContentPermissionByResource); + if (!authorizationResult.Succeeded) + { + return Forbid(); + } + //current permissions explicitly assigned to this content item var contentPermissions = _contentService.GetPermissions(content) .ToDictionary(x => x.UserGroupId, x => x); @@ -222,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(); @@ -338,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) { @@ -359,7 +368,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [EnsureUserPermissionForContent("id")] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] [DetermineAmbiguousActionByPassingParameters] public ContentItemDisplay GetById(Guid id) { @@ -380,7 +389,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [EnsureUserPermissionForContent("id")] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionBrowseById)] [DetermineAmbiguousActionByPassingParameters] public ContentItemDisplay GetById(Udi id) { @@ -629,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"); @@ -657,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); @@ -667,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 @@ -802,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"), @@ -818,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"), @@ -1189,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, @@ -1493,8 +1481,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)); @@ -1541,7 +1528,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) @@ -1587,7 +1574,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)); @@ -1600,8 +1587,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [EnsureUserPermissionForContent("sorted.ParentId", 'S')] - public IActionResult PostSort(ContentSortOrder sorted) + public async Task PostSort(ContentSortOrder sorted) { if (sorted == null) { @@ -1614,12 +1600,18 @@ namespace Umbraco.Web.BackOffice.Controllers return Ok(); } + // Authorize... + var resource = new ContentPermissionsResource(_contentService.GetById(sorted.ParentId), ActionSort.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, resource, AuthorizationPolicies.ContentPermissionByResource); + 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"); @@ -1641,9 +1633,16 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [EnsureUserPermissionForContent("move.ParentId", 'M')] - public IActionResult PostMove(MoveOrCopy move) + public async Task PostMove(MoveOrCopy move) { + // Authorize... + var resource = new ContentPermissionsResource(_contentService.GetById(move.ParentId), ActionMove.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, resource, AuthorizationPolicies.ContentPermissionByResource); + if (!authorizationResult.Succeeded) + { + return Forbid(); + } + var toMove = ValidateMoveOrCopy(move); _contentService.Move(toMove, move.ParentId, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); @@ -1656,9 +1655,16 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [EnsureUserPermissionForContent("copy.ParentId", 'C')] - public IActionResult PostCopy(MoveOrCopy copy) + public async Task PostCopy(MoveOrCopy copy) { + // Authorize... + var resource = new ContentPermissionsResource(_contentService.GetById(copy.ParentId), ActionCopy.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, resource, AuthorizationPolicies.ContentPermissionByResource); + 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)); @@ -1671,14 +1677,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 resource = new ContentPermissionsResource(foundContent, ActionUnpublish.ActionLetter); + var authorizationResult = await _authorizationService.AuthorizeAsync(User, resource, AuthorizationPolicies.ContentPermissionByResource); + if (!authorizationResult.Succeeded) + { + return Forbid(); + } var languageCount = _allLangs.Value.Count(); if (model.Cultures.Length == 0 || model.Cultures.Length == languageCount) @@ -2269,7 +2284,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(); @@ -2361,7 +2376,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 = "*") { @@ -2393,7 +2408,7 @@ namespace Umbraco.Web.BackOffice.Controllers throw HttpResponseException.CreateValidationErrorResponse(notificationModel); } - [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] + [Authorize(Policy = AuthorizationPolicies.ContentPermissionProtectById)] [HttpGet] public IActionResult GetPublicAccess(int contentId) { @@ -2442,7 +2457,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) { @@ -2509,7 +2524,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/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index 317464e46f..b00c1e2d33 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -33,6 +33,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.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -45,7 +47,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; @@ -130,7 +132,7 @@ namespace Umbraco.Web.BackOffice.Controllers } [HttpGet] - [UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] public bool HasContentNodes(int id) { return _contentTypeService.HasContentNodes(id); @@ -217,10 +219,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(); @@ -230,10 +229,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" }; @@ -274,10 +270,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); @@ -517,7 +510,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/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 41e5cfb589..d96708b11d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -21,7 +21,8 @@ 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; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { @@ -30,7 +31,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 IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; diff --git a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs index 24c5b1cb24..0ffe64e251 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; @@ -17,6 +18,7 @@ using Umbraco.Core.Serialization; using Umbraco.Core.Services; 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; @@ -32,7 +34,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; @@ -415,8 +417,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, - Constants.Applications.Settings, Constants.Applications.Packages)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessForDataTypeReading)] public IEnumerable GetAll() { return _dataTypeService @@ -431,8 +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, - Constants.Applications.Settings, Constants.Applications.Packages)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessForDataTypeReading)] public IDictionary> GetGroupedDataTypes() { var dataTypes = _dataTypeService @@ -463,9 +463,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, - Constants.Applications.Settings, Constants.Applications.Packages)] - + [Authorize(Policy = AuthorizationPolicies.SectionAccessForDataTypeReading)] public IDictionary> GetGroupedPropertyEditors() { var datatypes = new List(); @@ -496,9 +494,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, - Constants.Applications.Settings, Constants.Applications.Packages)] - + [Authorize(Policy = AuthorizationPolicies.SectionAccessForDataTypeReading)] 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..c7f86e12a1 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.Common.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..21b205de0f 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; @@ -12,6 +13,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; 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; @@ -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/LogController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs index 4d816624de..acdd9721e4 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; @@ -12,6 +13,7 @@ using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Security; @@ -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/MacrosController.cs b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs index e4ef90b30c..3ca89fa5ff 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 Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.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/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 0e97ed84af..409967fb67 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -40,6 +40,9 @@ using Umbraco.Web.Common.Exceptions; using Umbraco.Web.ContentApps; 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 { @@ -48,7 +51,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; @@ -65,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( @@ -87,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; @@ -108,6 +113,7 @@ namespace Umbraco.Web.BackOffice.Controllers _logger = loggerFactory.CreateLogger(); _imageUrlGenerator = imageUrlGenerator; _serializer = serializer; + _authorizationService = authorizationService; } /// @@ -165,7 +171,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [EnsureUserPermissionForMedia("id")] + [Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)] [DetermineAmbiguousActionByPassingParameters] public MediaItemDisplay GetById(int id) { @@ -186,7 +192,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [EnsureUserPermissionForMedia("id")] + [Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)] [DetermineAmbiguousActionByPassingParameters] public MediaItemDisplay GetById(Guid id) { @@ -207,7 +213,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] - [EnsureUserPermissionForMedia("id")] + [Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)] [DetermineAmbiguousActionByPassingParameters] public MediaItemDisplay GetById(Udi id) { @@ -430,7 +436,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// /// - [EnsureUserPermissionForMedia("id")] + [Authorize(Policy = AuthorizationPolicies.MediaPermissionPathById)] [HttpPost] public IActionResult DeleteById(int id) { @@ -471,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 MediaPermissionsResourceRequirement(); + 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; @@ -608,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) { @@ -622,14 +634,21 @@ namespace Umbraco.Web.BackOffice.Controllers return Ok(); } - var mediaService = _mediaService; + // Authorize... + var requirement = new MediaPermissionsResourceRequirement(); + 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"); @@ -643,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); } @@ -680,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)) @@ -704,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) @@ -729,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. @@ -763,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()) @@ -772,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, @@ -828,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; @@ -861,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 MediaPermissionsResourceRequirement(); + 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; } @@ -892,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); @@ -912,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); @@ -940,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/Controllers/MediaTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs index cd834e79fb..334b1adbe8 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; @@ -12,6 +13,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Strings; 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; @@ -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/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 55042d458b..a97ed9c2ad 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; @@ -27,6 +28,7 @@ using Umbraco.Extensions; 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; @@ -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/MemberGroupController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs index d3d06132d3..a7cbaf96c1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.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.Mapping; @@ -9,6 +10,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; 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; @@ -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..e203386958 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.Common.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/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index 69d23d606b..36bf4d2fca 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; @@ -14,6 +15,7 @@ using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Security; @@ -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..961ec388f7 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.Common.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/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index b8db09f9b7..dc565e7e5c 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 { @@ -64,7 +66,7 @@ namespace Umbraco.Web.BackOffice.Controllers _viewEngines = viewEngines; } - [UmbracoBackOfficeAuthorize(redirectToUmbracoLogin: true, requireApproval : false)] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccessWithoutApproval)] [DisableBrowserCache] public ActionResult Index() { @@ -107,7 +109,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/RelationController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs index bf40e5722f..5646c7f1aa 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs @@ -3,6 +3,7 @@ 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; @@ -11,6 +12,7 @@ using Umbraco.Core.Services; 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; @@ -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; @@ -56,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(); - } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs index 1ab5bd9bfa..b2706babee 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 Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.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..fe75cf5a0a 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; @@ -11,6 +12,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Strings; 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; @@ -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/Controllers/TinyMceController.cs b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs index 4bae8970bc..edaaa4f1e3 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; @@ -17,16 +18,13 @@ using Umbraco.Core.Strings; 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 { [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/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs index e3d779d61d..a807c663d0 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; @@ -14,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] - [UmbracoBackOfficeAuthorize] + [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [DisableBrowserCache] - [UmbracoWebApiRequireHttps] + [UmbracoRequireHttps] [CheckIfUserTicketDataIsStale] [MiddlewareFilter(typeof(UnhandledExceptionLoggerFilter))] public abstract class UmbracoAuthorizedApiController : UmbracoApiController diff --git a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs index 4cba5064d1..64aef74257 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 Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.Authorization; namespace Umbraco.Web.BackOffice.Controllers { [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] - [UmbracoApplicationAuthorize(Constants.Applications.Users)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)] [PrefixlessBodyModelValidator] public class UserGroupsController : UmbracoAuthorizedJsonController { @@ -164,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); @@ -178,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/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index f187ec22ce..809afff364 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 Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.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 @@ -65,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( @@ -90,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; @@ -113,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(); } @@ -138,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); @@ -191,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); @@ -230,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); @@ -248,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) @@ -365,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); @@ -449,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); @@ -603,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); @@ -717,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(); @@ -748,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(); @@ -772,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(); @@ -805,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(); @@ -831,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 5b78179f4c..d13a908034 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,9 +10,12 @@ 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; using Umbraco.Web.Common.AspNetCore; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.Security; namespace Umbraco.Extensions @@ -57,7 +61,7 @@ namespace Umbraco.Extensions services.GetRequiredService())); services.TryAddScoped, DefaultUserConfirmation>(); services.TryAddScoped, UserClaimsPrincipalFactory>(); - + // CUSTOM: services.TryAddScoped(); services.TryAddScoped(); @@ -74,5 +78,350 @@ namespace Umbraco.Extensions return new BackOfficeIdentityBuilder(services); } + + /// + /// Add authorization handlers and policies + /// + /// + 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. + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddAuthorization(o => CreatePolicies(o, backOfficeAuthenticationScheme)); + } + + 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); + 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); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(Constants.System.RecycleBinContent, ActionDelete.ActionLetter)); + }); + + options.AddPolicy(AuthorizationPolicies.ContentPermissionAdministrationById, policy => + { + 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(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter)); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionProtect.ActionLetter, "contentId")); + }); + + options.AddPolicy(AuthorizationPolicies.ContentPermissionRollbackById, policy => + { + 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(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionPublish.ActionLetter)); + }); + + options.AddPolicy(AuthorizationPolicies.ContentPermissionBrowseById, policy => + { + 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(backOfficeAuthenticationScheme); + policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); + }); + + options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new BackOfficeRequirement()); + }); + + options.AddPolicy(AuthorizationPolicies.BackOfficeAccessWithoutApproval, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new BackOfficeRequirement(false)); + }); + + options.AddPolicy(AuthorizationPolicies.AdminUserEditsRequireAdmin, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new AdminUsersRequirement()); + policy.Requirements.Add(new AdminUsersRequirement("userIds")); + }); + + options.AddPolicy(AuthorizationPolicies.UserBelongsToUserGroupInRequest, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new UserGroupRequirement()); + policy.Requirements.Add(new UserGroupRequirement("userGroupIds")); + }); + + options.AddPolicy(AuthorizationPolicies.DenyLocalLoginIfConfigured, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new DenyLocalLoginRequirement()); + }); + + options.AddPolicy(AuthorizationPolicies.SectionAccessContent, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Content)); + }); + + options.AddPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Content, Constants.Applications.Media)); + }); + + options.AddPolicy(AuthorizationPolicies.SectionAccessUsers, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Users)); + }); + + options.AddPolicy(AuthorizationPolicies.SectionAccessForTinyMce, policy => + { + 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(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Media)); + }); + + options.AddPolicy(AuthorizationPolicies.SectionAccessMembers, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Members)); + }); + + options.AddPolicy(AuthorizationPolicies.SectionAccessPackages, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement(Constants.Applications.Packages)); + }); + + options.AddPolicy(AuthorizationPolicies.SectionAccessSettings, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + 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 + // 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(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(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(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement( + 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(backOfficeAuthenticationScheme); + policy.Requirements.Add(new SectionRequirement( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + Constants.Applications.Settings, Constants.Applications.Packages)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDocuments, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Content)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessUsers, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Users)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViews, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.PartialViews)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessPartialViewMacros, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.PartialViewMacros)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessPackages, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Packages)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessLogs, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.LogViewer)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDataTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.DataTypes)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessTemplates, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Templates)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMemberTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberTypes)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessRelationTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.RelationTypes)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.DocumentTypes)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMemberGroups, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberGroups)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMediaTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MediaTypes)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMacros, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Macros)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessLanguages, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Languages)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDictionary, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary, Constants.Trees.Dictionary)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.Dictionary, Constants.Trees.Templates)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.DocumentTypes, Constants.Trees.Content)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MediaTypes, Constants.Trees.Media)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessMembersOrMemberTypes, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add(new TreeRequirement(Constants.Trees.MemberTypes, Constants.Trees.Members)); + }); + + options.AddPolicy(AuthorizationPolicies.TreeAccessAnySchemaTypes, policy => + { + 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(backOfficeAuthenticationScheme); + 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/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs index 76fd4a46f7..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; @@ -19,6 +20,7 @@ namespace Umbraco.Extensions .AddRuntimeMinifier() .AddBackOffice() .AddBackOfficeIdentity() + .AddBackOfficeAuthorizationPolicies() .AddMiniProfiler() .AddMvcAndRazor() .AddWebServer() @@ -53,6 +55,17 @@ namespace Umbraco.Extensions return builder; } + public static IUmbracoBuilder AddBackOfficeAuthorizationPolicies(this IUmbracoBuilder builder, string backOfficeAuthenticationScheme = Umbraco.Core.Constants.Security.BackOfficeAuthenticationType) + { + 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; + } + public static IUmbracoBuilder AddPreviewSupport(this IUmbracoBuilder builder) { builder.Services.AddSignalR(); 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/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/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 2553232185..686023a478 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,8 @@ using Umbraco.Core.Models; 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; @@ -27,43 +31,49 @@ 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 IAuthorizationService _authorizationService; private readonly ILoggerFactory _loggerFactory; - private readonly ILocalizedTextService _textService; - private readonly IUserService _userService; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; public ContentSaveValidationFilter( ILoggerFactory loggerFactory, - IBackOfficeSecurityAccessor backofficeSecurityAccessor, - ILocalizedTextService textService, IContentService contentService, - IUserService userService, - 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)); _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)); + _authorizationService = authorizationService; } - public void OnActionExecuting(ActionExecutingContext context) + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // on executing... + await OnActionExecutingAsync(context); + + if (context.Result == null) + { + //need to pass the execution to next if a result was not set + await 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); + var contentItemValidator = new ContentSaveModelValidator(_loggerFactory.CreateLogger(), _propertyValidationService); if (!ValidateAtLeastOneVariantIsBeingSaved(model, context)) return; if (!contentItemValidator.ValidateExistingContent(model, context)) return; - if (!ValidateUserAccess(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)) @@ -74,9 +84,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 +91,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,8 +110,9 @@ namespace Umbraco.Web.BackOffice.Filters /// /// /// - private bool ValidateUserAccess(ContentItemSave contentItem, ActionExecutingContext actionContext, - IBackOfficeSecurity backofficeSecurity) + private async Task ValidateUserAccessAsync( + ContentItemSave contentItem, + 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. @@ -210,42 +219,26 @@ 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 resource = contentToCheck == null + ? new ContentPermissionsResource(contentToCheck, contentIdToCheck, permissionToCheck) + : new ContentPermissionsResource(contentToCheck, permissionToCheck); - if (accessResult == ContentPermissionsHelper.ContentAccess.NotFound) - { - actionContext.Result = new NotFoundResult(); - } + var authorizationResult = await _authorizationService.AuthorizeAsync( + actionContext.HttpContext.User, + resource, + AuthorizationPolicies.ContentPermissionByResource); - if (accessResult != ContentPermissionsHelper.ContentAccess.Granted) + if (!authorizationResult.Succeeded) { actionContext.Result = new ForbidResult(); + return false; } - return accessResult == ContentPermissionsHelper.ContentAccess.Granted; + return true; } + + } } } 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(); - } - } - } - - } -} 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.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs deleted file mode 100644 index cabc865d40..0000000000 --- a/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs +++ /dev/null @@ -1,182 +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 - { - 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) - { - - } - - } - } -} 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/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..3ba2b408ef 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,9 @@ 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.Common.Authorization; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Filters { @@ -21,39 +23,45 @@ 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; - private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; public MediaItemSaveValidationFilter( ILoggerFactory loggerFactory, - 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); + + if (context.Result == null) + { + //need to pass the execution to next if a result was not set + await 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); + var contentItemValidator = new MediaSaveModelValidator(_loggerFactory.CreateLogger(), _propertyValidationService); - if (ValidateUserAccess(model, context)) + if (await ValidateUserAccessAsync(model, context)) { //now do each validation step if (contentItemValidator.ValidateExistingContent(model, context)) @@ -68,7 +76,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 +108,16 @@ namespace Umbraco.Web.BackOffice.Filters return false; } - if (MediaController.CheckPermissions( - actionContext.HttpContext.Items, - _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, - _mediaService, _entityService, - contentIdToCheck, contentToCheck) == false) + var resource = contentToCheck == null + ? new MediaPermissionsResource(contentIdToCheck) + : new MediaPermissionsResource(contentToCheck); + + var authorizationResult = await _authorizationService.AuthorizeAsync( + actionContext.HttpContext.User, + resource, + AuthorizationPolicies.MediaPermissionByResource); + + if (!authorizationResult.Succeeded) { actionContext.Result = new ForbidResult(); return false; @@ -112,11 +125,6 @@ namespace Umbraco.Web.BackOffice.Filters return true; } - - public void OnActionExecuted(ActionExecutedContext 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.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.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/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.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/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 - }; - } - - - } - } - } -} 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); - - } - } - } -} 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(); } diff --git a/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs b/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs index 768ede1787..002cfe4d7b 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 Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.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..0908522d9e 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.Common.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..e232bf03b9 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs @@ -1,5 +1,6 @@ using System; 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.Services; using Umbraco.Web.Actions; 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; @@ -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..16dd446d49 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.Common.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/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.BackOffice/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs index 5c8312c058..8b5286bdd2 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; 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.Services; using Umbraco.Web.Actions; 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; @@ -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..ab2bfdb8d4 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 Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.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..ffa9e00b0e 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Models; @@ -7,6 +8,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Actions; 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; @@ -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..ecd1c954ac 100644 --- a/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/LanguageTreeController.cs @@ -1,7 +1,9 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core.Services; 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; @@ -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..b03b2d9926 100644 --- a/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/LogViewerTreeController.cs @@ -1,7 +1,9 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Umbraco.Core.Services; 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; @@ -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..518c1b5495 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 Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.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/MediaTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs index ab332e3843..ece4013d0b 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.Common.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/MediaTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs index 4df81b7023..cd64e23067 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 Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.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..817b32f301 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs @@ -1,17 +1,19 @@ 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.Filters; using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Authorization; using Umbraco.Web.Models.Trees; using Umbraco.Web.Trees; 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/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs index 31ab66908c..4ebd8f7cc5 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 Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.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] diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs index c9e340617e..be400bef39 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Models; @@ -7,6 +8,7 @@ using Umbraco.Core.Services; 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; @@ -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..5c96bb4d64 100644 --- a/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/PackagesTreeController.cs @@ -1,7 +1,9 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Umbraco.Core.Services; 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; @@ -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..484ea21b2f 100644 --- a/src/Umbraco.Web.BackOffice/Trees/PartialViewMacrosTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/PartialViewMacrosTreeController.cs @@ -1,7 +1,9 @@ -using Umbraco.Core.IO; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Core.IO; using Umbraco.Core.Services; 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; @@ -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..b648bd797f 100644 --- a/src/Umbraco.Web.BackOffice/Trees/PartialViewsTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/PartialViewsTreeController.cs @@ -1,7 +1,9 @@ -using Umbraco.Core.IO; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Core.IO; using Umbraco.Core.Services; 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; @@ -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..a36c2f36a9 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 Microsoft.AspNetCore.Authorization; +using Umbraco.Web.Common.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..361875a41b 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; @@ -10,6 +11,7 @@ using Umbraco.Extensions; using Umbraco.Web.Actions; 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; @@ -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..960ed76ac5 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.Common.Attributes; +using Umbraco.Web.Common.Authorization; 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] diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs new file mode 100644 index 0000000000..56070f5033 --- /dev/null +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -0,0 +1,82 @@ +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 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); + + // 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); + public const string ContentPermissionRollbackById = nameof(ContentPermissionRollbackById); + public const string ContentPermissionProtectById = nameof(ContentPermissionProtectById); + 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 + + 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); + + // Single tree access + + 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); + + // Custom access based on multiple trees + + 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 = nameof(TreeAccessAnyContentOrTypes); + } +} diff --git a/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs new file mode 100644 index 0000000000..3f2fc0b6bb --- /dev/null +++ b/src/Umbraco.Web.Common/Authorization/FeatureAuthorizeHandler.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; +using System; +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)) + { + throw new InvalidOperationException("This authorization handler can only be applied to controllers routed with endpoint routing"); + } + + var actionDescriptor = endpoint.Metadata.GetMetadata(); + var controllerType = actionDescriptor.ControllerTypeInfo.AsType(); + return _umbracoFeatures.IsControllerEnabled(controllerType); + } + } +} 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/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 787da05ca4..9f9e2b19be 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,11 +14,13 @@ 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 { + // 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/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 596c42e3b3..caf4132664 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..e8e3e2c329 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +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) + { + // 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 => + { + 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); - } - } - } -} diff --git a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs deleted file mode 100644 index 1f4abbaa25..0000000000 --- a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -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 }; - } - } -} diff --git a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeFilter.cs deleted file mode 100644 index 0c30b25ced..0000000000 --- a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeFilter.cs +++ /dev/null @@ -1,111 +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 IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IRuntimeState _runtimeState; - private readonly LinkGenerator _linkGenerator; - private readonly bool _redirectToUmbracoLogin; - private string _redirectUrl; - - private UmbracoBackOfficeAuthorizeFilter( - IHostingEnvironment hostingEnvironment, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IRuntimeState runtimeState, - LinkGenerator linkGenerator, - bool redirectToUmbracoLogin, bool requireApproval, string redirectUrl) - { - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); - _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _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, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IRuntimeState runtimeState, LinkGenerator linkGenerator, - string redirectUrl) : this(hostingEnvironment, backOfficeSecurityAccessor, runtimeState, linkGenerator, false, false, redirectUrl) - { - } - - public UmbracoBackOfficeAuthorizeFilter( - IHostingEnvironment hostingEnvironment, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IRuntimeState runtimeState, LinkGenerator linkGenerator, - bool redirectToUmbracoLogin, bool requireApproval) : this(hostingEnvironment, backOfficeSecurityAccessor, 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 - || _backOfficeSecurityAccessor?.BackOfficeSecurity.ValidateCurrentUser(false, _requireApproval) == ValidateRequestAttempt.Success; - } - catch (Exception) - { - return false; - } - } - } -} diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs index 4019f462eb..fc247af55c 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs @@ -9,10 +9,11 @@ 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 private readonly IUmbracoWebsiteSecurity _websiteSecurity; public UmbracoMemberAuthorizeFilter(IUmbracoWebsiteSecurity websiteSecurity) 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 bddb4fd015..04f743144e 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)) { } diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index d5980c10b5..f20ab5ab75 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -97,8 +97,7 @@ namespace Umbraco.Web.Common.Runtime builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(factory => new LegacyPasswordSecurity()); + builder.Services.AddUnique(); } } } diff --git a/src/Umbraco.Web.Website/Controllers/UmbracoAuthorizedController.cs b/src/Umbraco.Web.Website/Controllers/UmbracoAuthorizedController.cs deleted file mode 100644 index b2611848df..0000000000 --- a/src/Umbraco.Web.Website/Controllers/UmbracoAuthorizedController.cs +++ /dev/null @@ -1,18 +0,0 @@ -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. - /// - [UmbracoBackOfficeAuthorize] - [DisableBrowserCache] - public abstract class UmbracoAuthorizedController : UmbracoController - { - - } -}