From acc71b6d458783d3b6f21c199d302427419aa0c2 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Mon, 22 Jan 2024 15:58:18 +0100 Subject: [PATCH] Add management api delete document endpoints (#15600) * Added Delete document endpoint * Added delete from recylebin endpoint Added/restructured unittests * Clarifications * Moved document specific mapping from base recyclebin to document recyclebin * Refactor OperationStatusResult mapping trough inheritance --------- Co-authored-by: Sven Geusens --- .../Document/DeleteDocumentController.cs | 57 ++++++++++++++++++ .../DeleteDocumentRecycleBinController.cs | 59 +++++++++++++++++++ .../DocumentRecycleBinControllerBase.cs | 2 + .../RecycleBin/RecycleBinControllerBase.cs | 3 +- .../Services/ContentEditingService.cs | 5 +- .../Services/ContentEditingServiceBase.cs | 30 +++++++--- .../Services/IContentEditingService.cs | 10 +++- .../ContentEditingServiceTests.Delete.cs | 32 +++++----- ...ditingServiceTests.DeleteFromRecycleBin.cs | 47 +++++++++++++++ ...tentEditingServiceTests.MoveToRecyleBin.cs | 2 +- .../Umbraco.Tests.Integration.csproj | 5 +- 11 files changed, 224 insertions(+), 28 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Document/DeleteDocumentController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DeleteDocumentRecycleBinController.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.DeleteFromRecycleBin.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/DeleteDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/DeleteDocumentController.cs new file mode 100644 index 0000000000..b1e4caa1de --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/DeleteDocumentController.cs @@ -0,0 +1,57 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.Document; + +[ApiVersion("1.0")] +public class DeleteDocumentController : DocumentControllerBase +{ + private readonly IAuthorizationService _authorizationService; + private readonly IContentEditingService _contentEditingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public DeleteDocumentController( + IAuthorizationService authorizationService, + IContentEditingService contentEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _authorizationService = authorizationService; + _contentEditingService = contentEditingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpDelete("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Delete(Guid id) + { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionDelete.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + + Attempt result = await _contentEditingService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor)); + + return result.Success + ? Ok() + : ContentEditingOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DeleteDocumentRecycleBinController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DeleteDocumentRecycleBinController.cs new file mode 100644 index 0000000000..d152ba4c95 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DeleteDocumentRecycleBinController.cs @@ -0,0 +1,59 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Security.Authorization.Content; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin; + +[ApiVersion("1.0")] +public class DeleteDocumentRecycleBinController : DocumentRecycleBinControllerBase +{ + private readonly IAuthorizationService _authorizationService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IContentEditingService _contentEditingService; + + public DeleteDocumentRecycleBinController( + IEntityService entityService, + IAuthorizationService authorizationService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IContentEditingService contentEditingService) + : base(entityService) + { + _authorizationService = authorizationService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _contentEditingService = contentEditingService; + } + + [HttpDelete("{id:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Delete(Guid id) + { + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionDelete.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); + + if (!authorizationResult.Succeeded) + { + return Forbidden(); + } + + Attempt result = await _contentEditingService.DeleteFromRecycleBinAsync(id, CurrentUserKey(_backOfficeSecurityAccessor)); + + return result.Success + ? Ok() + : ContentEditingOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs index 53db83fd73..47dea7fcbc 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -9,6 +10,7 @@ using Umbraco.Cms.Api.Management.Controllers.RecycleBin; using Umbraco.Cms.Api.Management.Filters; using Umbraco.Cms.Api.Management.ViewModels.RecycleBin; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs index be6740b031..8bd452ab1e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs @@ -6,11 +6,12 @@ using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Api.Management.Services.Paging; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Content; using Umbraco.Cms.Api.Management.ViewModels.RecycleBin; namespace Umbraco.Cms.Api.Management.Controllers.RecycleBin; -public abstract class RecycleBinControllerBase : ManagementApiControllerBase +public abstract class RecycleBinControllerBase : ContentControllerBase where TItem : RecycleBinItemResponseModel, new() { private readonly IEntityService _entityService; diff --git a/src/Umbraco.Core/Services/ContentEditingService.cs b/src/Umbraco.Core/Services/ContentEditingService.cs index d0f004310d..ae5ee0c594 100644 --- a/src/Umbraco.Core/Services/ContentEditingService.cs +++ b/src/Umbraco.Core/Services/ContentEditingService.cs @@ -80,8 +80,11 @@ internal sealed class ContentEditingService public async Task> MoveToRecycleBinAsync(Guid key, Guid userKey) => await HandleMoveToRecycleBinAsync(key, userKey); + public async Task> DeleteFromRecycleBinAsync(Guid key, Guid userKey) + => await HandleDeleteAsync(key, userKey, true); + public async Task> DeleteAsync(Guid key, Guid userKey) - => await HandleDeleteAsync(key, userKey); + => await HandleDeleteAsync(key, userKey, false); public async Task> MoveAsync(Guid key, Guid? parentKey, Guid userKey) => await HandleMoveAsync(key, parentKey, userKey); diff --git a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs index 4aa4246a96..c7c589b922 100644 --- a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs @@ -103,13 +103,17 @@ internal abstract class ContentEditingServiceBase> HandleMoveToRecycleBinAsync(Guid key, Guid userKey) - => await HandleDeletionAsync(key, userKey, false, MoveToRecycleBin); + => await HandleDeletionAsync(key, userKey, ContentTrashStatusRequirement.MustNotBeTrashed, MoveToRecycleBin); - protected async Task> HandleDeleteAsync(Guid key, Guid userKey) - => await HandleDeletionAsync(key, userKey, true, Delete); + protected async Task> HandleDeleteAsync(Guid key, Guid userKey, bool mustBeTrashed = true) + => await HandleDeletionAsync(key, userKey, mustBeTrashed ? ContentTrashStatusRequirement.MustBeTrashed : ContentTrashStatusRequirement.Irrelevant, Delete); - // helper method to perform move-to-recycle-bin and delete for content as they are very much handled in the same way - private async Task> HandleDeletionAsync(Guid key, Guid userKey, bool mustBeTrashed, Func performDelete) + // helper method to perform move-to-recycle-bin, delete-from-recycle-bin and delete for content as they are very much handled in the same way + // IContentEditingService methods hitting this (ContentTrashStatusRequirement, calledFunction): + // DeleteAsync (irrelevant, Delete) + // MoveToRecycleBinAsync (MustNotBeTrashed, MoveToRecycleBin) + // DeleteFromRecycleBinAsync (MustBeTrashed, Delete) + private async Task> HandleDeletionAsync(Guid key, Guid userKey, ContentTrashStatusRequirement trashStatusRequirement, Func performDelete) { using ICoreScope scope = CoreScopeProvider.CreateCoreScope(); TContent? content = ContentService.GetById(key); @@ -118,9 +122,11 @@ internal abstract class ContentEditingServiceBase(status, content)); @@ -470,4 +476,14 @@ internal abstract class ContentEditingServiceBase GetPropertyTypesByAlias(TContentType contentType) => contentType.CompositionPropertyTypes.ToDictionary(pt => pt.Alias); + + /// + /// Should never be made public, serves the purpose of a nullable bool but more readable. + /// + private enum ContentTrashStatusRequirement + { + Irrelevant, + MustBeTrashed, + MustNotBeTrashed + } } diff --git a/src/Umbraco.Core/Services/IContentEditingService.cs b/src/Umbraco.Core/Services/IContentEditingService.cs index bac154975a..b00f768ee5 100644 --- a/src/Umbraco.Core/Services/IContentEditingService.cs +++ b/src/Umbraco.Core/Services/IContentEditingService.cs @@ -14,11 +14,19 @@ public interface IContentEditingService Task> MoveToRecycleBinAsync(Guid key, Guid userKey); - Task> DeleteAsync(Guid key, Guid userKey); + /// + /// Deletes a Content Item if it is in the recycle bin. + /// + Task> DeleteFromRecycleBinAsync(Guid key, Guid userKey); Task> MoveAsync(Guid key, Guid? parentKey, Guid userKey); Task> CopyAsync(Guid key, Guid? parentKey, bool relateToOriginal, bool includeDescendants, Guid userKey); Task SortAsync(Guid? parentKey, IEnumerable sortingModels, Guid userKey); + + /// + /// Deletes a Content Item whether it is in the recycle bin or not. + /// + Task> DeleteAsync(Guid key, Guid userKey); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs index 225265d2f1..1639fbf726 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs @@ -8,11 +8,26 @@ public partial class ContentEditingServiceTests { [TestCase(true)] [TestCase(false)] - public async Task Can_Delete(bool variant) + public async Task Can_Delete_FromRecycleBin(bool variant) { var content = await (variant ? CreateVariantContent() : CreateInvariantContent()); await ContentEditingService.MoveToRecycleBinAsync(content.Key, Constants.Security.SuperUserKey); + var result = await ContentEditingService.DeleteFromRecycleBinAsync(content.Key, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status); + + // re-get and verify deletion + content = await ContentEditingService.GetAsync(content.Key); + Assert.IsNull(content); + } + + [TestCase(true)] + [TestCase(false)] + public async Task Can_Delete_FromOutsideOfRecycleBin(bool variant) + { + var content = await (variant ? CreateVariantContent() : CreateInvariantContent()); + var result = await ContentEditingService.DeleteAsync(content.Key, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status); @@ -29,19 +44,4 @@ public partial class ContentEditingServiceTests Assert.IsFalse(result.Success); Assert.AreEqual(ContentEditingOperationStatus.NotFound, result.Status); } - - [TestCase(true)] - [TestCase(false)] - public async Task Cannot_Delete_If_Not_In_Recycle_Bin(bool variant) - { - var content = await (variant ? CreateVariantContent() : CreateInvariantContent()); - - var result = await ContentEditingService.DeleteAsync(content.Key, Constants.Security.SuperUserKey); - Assert.IsFalse(result.Success); - Assert.AreEqual(ContentEditingOperationStatus.NotInTrash, result.Status); - - // re-get and verify that deletion did not happen - content = await ContentEditingService.GetAsync(content.Key); - Assert.IsNotNull(content); - } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.DeleteFromRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.DeleteFromRecycleBin.cs new file mode 100644 index 0000000000..32be47e00d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.DeleteFromRecycleBin.cs @@ -0,0 +1,47 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +public partial class ContentEditingServiceTests +{ + [TestCase(true)] + [TestCase(false)] + public async Task Can_DeleteFromRecycleBin_If_InsideRecycleBin(bool variant) + { + var content = await (variant ? CreateVariantContent() : CreateInvariantContent()); + await ContentEditingService.MoveToRecycleBinAsync(content.Key, Constants.Security.SuperUserKey); + + var result = await ContentEditingService.DeleteFromRecycleBinAsync(content.Key, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status); + + // re-get and verify deletion + content = await ContentEditingService.GetAsync(content.Key); + Assert.IsNull(content); + } + + [Test] + public async Task Cannot_Delete_FromRecycleBin_Non_Existing() + { + var result = await ContentEditingService.DeleteFromRecycleBinAsync(Guid.NewGuid(), Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.NotFound, result.Status); + } + + [TestCase(true)] + [TestCase(false)] + public async Task Cannot_Delete_FromRecycleBin_If_Not_In_Recycle_Bin(bool variant) + { + var content = await (variant ? CreateVariantContent() : CreateInvariantContent()); + + var result = await ContentEditingService.DeleteFromRecycleBinAsync(content.Key, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.NotInTrash, result.Status); + + // re-get and verify that deletion did not happen + content = await ContentEditingService.GetAsync(content.Key); + Assert.IsNotNull(content); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.MoveToRecyleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.MoveToRecyleBin.cs index 3d39c39689..3dd806abfe 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.MoveToRecyleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.MoveToRecyleBin.cs @@ -25,7 +25,7 @@ public partial class ContentEditingServiceTests [Test] public async Task Cannot_Move_Non_Existing_To_Recycle_Bin() { - var result = await ContentEditingService.DeleteAsync(Guid.NewGuid(), Constants.Security.SuperUserKey); + var result = await ContentEditingService.MoveToRecycleBinAsync(Guid.NewGuid(), Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentEditingOperationStatus.NotFound, result.Status); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 352fa7d742..f9bb577d76 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -58,7 +58,7 @@ ContentEditingServiceTests.cs - + ContentEditingServiceTests.cs @@ -121,6 +121,9 @@ MediaTypeEditingServiceTests.cs + + ContentEditingServiceTests.cs +