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