diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs index 4b585e78b9..a97a73e712 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs @@ -17,21 +17,18 @@ public abstract class UpdateDocumentControllerBase : DocumentControllerBase protected async Task HandleRequest(Guid id, UpdateDocumentRequestModel requestModel, Func> authorizedHandler) { - // TODO This have temporarily been uncommented, to support the client sends values from all cultures, even when the user do not have access to the languages. - // The values are ignored in the ContentEditingService + // We intentionally don't pass in cultures here. + // This is to support the client sending values for all cultures even if the user doesn't have access to the language. + // Values for unauthorized languages are later ignored in the ContentEditingService. + AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + User, + ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id), + AuthorizationPolicies.ContentPermissionByResource); - // IEnumerable cultures = requestModel.Variants - // .Where(v => v.Culture is not null) - // .Select(v => v.Culture!); - // AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( - // User, - // ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id, cultures), - // AuthorizationPolicies.ContentPermissionByResource); - // - // if (!authorizationResult.Succeeded) - // { - // return Forbidden(); - // } + if (authorizationResult.Succeeded is false) + { + return Forbidden(); + } return await authorizedHandler(); } diff --git a/tests/Umbraco.Tests.Common/TestHelpers/DocumentUpdateHelper.cs b/tests/Umbraco.Tests.Common/TestHelpers/DocumentUpdateHelper.cs new file mode 100644 index 0000000000..d3a8546fd8 --- /dev/null +++ b/tests/Umbraco.Tests.Common/TestHelpers/DocumentUpdateHelper.cs @@ -0,0 +1,31 @@ +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Tests.Common.TestHelpers; + +public static class DocumentUpdateHelper +{ + public static UpdateDocumentRequestModel CreateInvariantDocumentUpdateRequestModel(ContentCreateModel createModel) + { + var updateRequestModel = new UpdateDocumentRequestModel(); + + updateRequestModel.Template = ReferenceByIdModel.ReferenceOrNull(createModel.TemplateKey); + updateRequestModel.Variants = + [ + new DocumentVariantRequestModel + { + Segment = null, + Culture = null, + Name = createModel.InvariantName!, + } + ]; + updateRequestModel.Values = createModel.InvariantProperties.Select(x => new DocumentValueModel + { + Alias = x.Alias, + Value = x.Value, + }); + + return updateRequestModel; + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs b/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs index 043ed9a13a..ab03d0e402 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs @@ -47,41 +47,56 @@ public abstract class ManagementApiTest : UmbracoTestServerTestBase protected virtual string Url => GetManagementApiUrl(MethodSelector); - protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, bool isAdmin) + protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, bool isAdmin) => + await AuthenticateClientAsync(client, + async userService => + { + IUser user; + if (isAdmin) + { + user = await userService.GetRequiredUserAsync(Constants.Security.SuperUserKey); + user.Username = user.Email = username; + userService.Save(user); + } + else + { + user = (await userService.CreateAsync( + Constants.Security.SuperUserKey, + new UserCreateModel + { + Email = username, + Name = username, + UserName = username, + UserGroupKeys = new HashSet(new[] { Constants.Security.EditorGroupKey }) + }, + true)).Result.CreatedUser; + } + + return (user, password); + }); + + + protected async Task AuthenticateClientAsync(HttpClient client, Func> createUser) { - Guid userKey = Constants.Security.SuperUserKey; + OpenIddictApplicationDescriptor backofficeOpenIddictApplicationDescriptor; var scopeProvider = GetRequiredService(); + + string? username; + string? password; + using (var scope = scopeProvider.CreateCoreScope()) { var userService = GetRequiredService(); using var serviceScope = GetRequiredService().CreateScope(); var userManager = serviceScope.ServiceProvider.GetRequiredService(); - IUser user; - if (isAdmin) - { - user = await userService.GetRequiredUserAsync(userKey); - user.Username = user.Email = username; - userService.Save(user); - } - else - { - user = (await userService.CreateAsync( - Constants.Security.SuperUserKey, - new UserCreateModel() - { - Email = username, - Name = username, - UserName = username, - UserGroupKeys = new HashSet(new[] { Constants.Security.EditorGroupKey }) - }, - true)).Result.CreatedUser; - userKey = user.Key; - } + var userCreationResult = await createUser(userService); + username = userCreationResult.user.Username; + password = userCreationResult.Password; + var userKey = userCreationResult.user.Key; - - var token = await userManager.GeneratePasswordResetTokenAsync(user); + var token = await userManager.GeneratePasswordResetTokenAsync(userCreationResult.user); var changePasswordAttempt = await userService.ChangePasswordAsync(userKey, @@ -99,6 +114,7 @@ public abstract class ManagementApiTest : UmbracoTestServerTestBase BackOfficeApplicationManager; backofficeOpenIddictApplicationDescriptor = backOfficeApplicationManager.BackofficeOpenIddictApplicationDescriptor(client.BaseAddress); + scope.Complete(); } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Policies/UpdateDocumentTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Policies/UpdateDocumentTests.cs new file mode 100644 index 0000000000..b7d15f2fd2 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Policies/UpdateDocumentTests.cs @@ -0,0 +1,153 @@ +using System.Linq.Expressions; +using System.Net; +using System.Text; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Document; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ContentPublishing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.TestHelpers; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Policies; + +public class UpdateDocumentTests : ManagementApiTest +{ + private IUserGroupService UserGroupService => GetRequiredService(); + + private IShortStringHelper ShortStringHelper => GetRequiredService(); + + private IJsonSerializer JsonSerializer => GetRequiredService(); + + private ITemplateService TemplateService => GetRequiredService(); + + private IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + private IContentPublishingService ContentPublishingService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + protected override Expression> MethodSelector => + x => x.Update(CancellationToken.None, Guid.Empty, null!); + + [Test] + public async Task UserWithoutPermissionCannotUpdate() + { + var userGroup = new UserGroup(ShortStringHelper) + { + Name = "Test", + Alias = "test", + Permissions = new HashSet { ActionBrowse.ActionLetter }, + HasAccessToAllLanguages = true, + StartContentId = -1, + StartMediaId = -1 + }; + userGroup.AddAllowedSection("content"); + userGroup.AddAllowedSection("media"); + + var groupCreationResult = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + Assert.IsTrue(groupCreationResult.Success); + + await AuthenticateClientAsync(Client, async userService => + { + var email = "test@test.com"; + var testUserCreateModel = new UserCreateModel + { + Email = email, + Name = "Test Mc.Gee", + UserName = email, + UserGroupKeys = new HashSet { groupCreationResult.Result.Key }, + }; + + var userCreationResult = + await userService.CreateAsync(Constants.Security.SuperUserKey, testUserCreateModel, true); + + Assert.IsTrue(userCreationResult.Success); + + return (userCreationResult.Result.CreatedUser, "1234567890"); + }); + + const string UpdatedName = "NewName"; + + var model = await CreateContent(); + var updateRequestModel = CreateRequestModel(model, UpdatedName); + + var response = await GetManagementApiResponse(model, updateRequestModel); + + AssertResponse(response, model, HttpStatusCode.Forbidden, model.InvariantName); + } + + [Test] + public async Task UserWithPermissionCanUpdate() + { + // "Default" version creates an editor that has permission to update content. + await AuthenticateClientAsync(Client, "editor@editor.com", "1234567890", false); + + const string UpdatedName = "NewName"; + + var model = await CreateContent(); + var updateRequestModel = CreateRequestModel(model, UpdatedName); + + var response = await GetManagementApiResponse(model, updateRequestModel); + + AssertResponse(response, model, HttpStatusCode.OK, UpdatedName); + } + + private async Task CreateContent() + { + var userKey = Constants.Security.SuperUserKey; + var template = TemplateBuilder.CreateTextPageTemplate(); + var templateAttempt = await TemplateService.CreateAsync(template, userKey); + Assert.IsTrue(templateAttempt.Success); + + var contentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType(defaultTemplateKey: template.Key); + var contentTypeAttempt = await ContentTypeEditingService.CreateAsync(contentTypeCreateModel, userKey); + Assert.IsTrue(contentTypeAttempt.Success); + + var textPage = ContentEditingBuilder.CreateSimpleContent(contentTypeAttempt.Result.Key); + textPage.TemplateKey = templateAttempt.Result.Key; + textPage.Key = Guid.NewGuid(); + var createContentResult = await ContentEditingService.CreateAsync(textPage, userKey); + Assert.IsTrue(createContentResult.Success); + + var publishResult = await ContentPublishingService.PublishAsync( + createContentResult.Result.Content!.Key, + [new() { Culture = "*" }], + userKey); + + Assert.IsTrue(publishResult.Success); + return textPage; + } + + private static UpdateDocumentRequestModel CreateRequestModel(ContentCreateModel model, string name) + { + var updateRequestModel = DocumentUpdateHelper.CreateInvariantDocumentUpdateRequestModel(model); + updateRequestModel.Variants.First().Name = name; + return updateRequestModel; + } + + private async Task GetManagementApiResponse(ContentCreateModel model, UpdateDocumentRequestModel updateRequestModel) + { + var url = GetManagementApiUrl(x => x.Update(CancellationToken.None, model.Key!.Value, null)); + var requestBody = new StringContent(JsonSerializer.Serialize(updateRequestModel), Encoding.UTF8, "application/json"); + return await Client.PutAsync(url, requestBody); + } + + private void AssertResponse(HttpResponseMessage response, ContentCreateModel model, HttpStatusCode expectedStatusCode, string expectedContentName) + { + Assert.That(response.StatusCode, Is.EqualTo(expectedStatusCode)); + var content = ContentService.GetById(model.Key!.Value); + Assert.IsNotNull(content); + Assert.That(content.Name, Is.EqualTo(expectedContentName)); + } +}