diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index d06062498a..6ba81172c8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -826,6 +826,7 @@ public class ContentController : ContentControllerBase /// The content id to copy /// The name of the blueprint /// + [Authorize(Policy = AuthorizationPolicies.ContentPermissionCreateBlueprintFromId)] [HttpPost] public ActionResult CreateBlueprintFromContent( [FromQuery] int contentId, @@ -881,8 +882,9 @@ public class ContentController : ContentControllerBase /// /// Saves content /// + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [FileUploadCleanupFilter] - [ContentSaveValidation] + [ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch public async Task?>?> PostSaveBlueprint( [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { @@ -2077,6 +2079,7 @@ public class ContentController : ContentControllerBase return Ok(); } + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [HttpDelete] [HttpPost] public IActionResult DeleteBlueprint(int id) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index bfd460bda4..0945d3459b 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -179,6 +179,13 @@ public static partial class UmbracoBuilderExtensions policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); }); + options.AddPolicy(AuthorizationPolicies.ContentPermissionCreateBlueprintFromId, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add( + new ContentPermissionsQueryStringRequirement(ActionCreateBlueprintFromContent.ActionLetter, "contentId")); + }); + options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index 100d089451..f7be9d129a 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -20,9 +20,12 @@ namespace Umbraco.Cms.Web.BackOffice.Filters; /// internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute { - public ContentSaveValidationAttribute() : base(typeof(ContentSaveValidationFilter)) => + public ContentSaveValidationAttribute(bool skipUserAccessValidation = false) + : base(typeof(ContentSaveValidationFilter)) + { Order = -3000; // More important than ModelStateInvalidFilter.FilterOrder - + Arguments = new object[] { skipUserAccessValidation }; + } private sealed class ContentSaveValidationFilter : IAsyncActionFilter { @@ -32,6 +35,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute private readonly ILocalizationService _localizationService; private readonly ILoggerFactory _loggerFactory; private readonly IPropertyValidationService _propertyValidationService; + private readonly bool _skipUserAccessValidation; public ContentSaveValidationFilter( @@ -40,7 +44,8 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute IPropertyValidationService propertyValidationService, IAuthorizationService authorizationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - ILocalizationService localizationService) + ILocalizationService localizationService, + bool skipUserAccessValidation) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); @@ -49,6 +54,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute _authorizationService = authorizationService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _localizationService = localizationService; + _skipUserAccessValidation = skipUserAccessValidation; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) @@ -88,7 +94,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute return; } - if (!await ValidateUserAccessAsync(model, context)) + if (_skipUserAccessValidation is false && await ValidateUserAccessAsync(model, context) is false) { return; } diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index cfad020a92..429cc77faf 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -22,6 +22,7 @@ public static class AuthorizationPolicies public const string ContentPermissionProtectById = nameof(ContentPermissionProtectById); public const string ContentPermissionBrowseById = nameof(ContentPermissionBrowseById); public const string ContentPermissionDeleteById = nameof(ContentPermissionDeleteById); + public const string ContentPermissionCreateBlueprintFromId = nameof(ContentPermissionCreateBlueprintFromId); public const string MediaPermissionByResource = nameof(MediaPermissionByResource); public const string MediaPermissionPathById = nameof(MediaPermissionPathById);