From c5243e562e6cc869459b12a993cdec57f1fc762b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 18 Sep 2024 13:10:15 +0200 Subject: [PATCH] Allow the client to send all content, with all languages, even when the user do not have permissions to save a specific language. (#17052) --- .../Document/CreateDocumentControllerBase.cs | 25 ++--- .../Document/UpdateDocumentControllerBase.cs | 25 ++--- .../Services/ContentEditingService.cs | 97 ++++++++++++++++++- 3 files changed, 123 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs index d6983964c533..669c2cdc933e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs @@ -18,18 +18,21 @@ protected CreateDocumentControllerBase(IAuthorizationService authorizationServic protected async Task HandleRequest(CreateDocumentRequestModel requestModel, Func> authorizedHandler) { - IEnumerable cultures = requestModel.Variants - .Where(v => v.Culture is not null) - .Select(v => v.Culture!); - AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( - User, - ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id, cultures), - AuthorizationPolicies.ContentPermissionByResource); + // 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 - if (!authorizationResult.Succeeded) - { - return Forbidden(); - } + // IEnumerable cultures = requestModel.Variants + // .Where(v => v.Culture is not null) + // .Select(v => v.Culture!); + // AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync( + // User, + // ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id, cultures), + // AuthorizationPolicies.ContentPermissionByResource); + // + // if (!authorizationResult.Succeeded) + // { + // return Forbidden(); + // } return await authorizedHandler(); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs index 2d41fe94fe66..4b585e78b925 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs @@ -17,18 +17,21 @@ protected UpdateDocumentControllerBase(IAuthorizationService authorizationServic protected async Task HandleRequest(Guid id, UpdateDocumentRequestModel requestModel, Func> authorizedHandler) { - 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); + // 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 - if (!authorizationResult.Succeeded) - { - return Forbidden(); - } + // 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(); + // } return await authorizedHandler(); } diff --git a/src/Umbraco.Core/Services/ContentEditingService.cs b/src/Umbraco.Core/Services/ContentEditingService.cs index 2ad8365bcfcf..bc15e7ea4410 100644 --- a/src/Umbraco.Core/Services/ContentEditingService.cs +++ b/src/Umbraco.Core/Services/ContentEditingService.cs @@ -1,9 +1,13 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services; @@ -12,7 +16,11 @@ internal sealed class ContentEditingService { private readonly ITemplateService _templateService; private readonly ILogger _logger; + private readonly IUserService _userService; + private readonly ILocalizationService _localizationService; + private readonly ILanguageService _languageService; + [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16.")] public ContentEditingService( IContentService contentService, IContentTypeService contentTypeService, @@ -24,10 +32,46 @@ public ContentEditingService( IUserIdKeyResolver userIdKeyResolver, ITreeEntitySortingService treeEntitySortingService, IContentValidationService contentValidationService) + : this( + contentService, + contentTypeService, + propertyEditorCollection, + dataTypeService, + templateService, + logger, + scopeProvider, + userIdKeyResolver, + treeEntitySortingService, + contentValidationService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService() + ) + { + + } + + public ContentEditingService( + IContentService contentService, + IContentTypeService contentTypeService, + PropertyEditorCollection propertyEditorCollection, + IDataTypeService dataTypeService, + ITemplateService templateService, + ILogger logger, + ICoreScopeProvider scopeProvider, + IUserIdKeyResolver userIdKeyResolver, + ITreeEntitySortingService treeEntitySortingService, + IContentValidationService contentValidationService, + IUserService userService, + ILocalizationService localizationService, + ILanguageService languageService) : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, contentValidationService, treeEntitySortingService) { _templateService = templateService; _logger = logger; + _userService = userService; + _localizationService = localizationService; + _languageService = languageService; } public async Task GetAsync(Guid key) @@ -65,7 +109,7 @@ public async Task> C ContentEditingOperationStatus validationStatus = result.Status; ContentValidationResult validationResult = result.Result.ValidationResult; - IContent content = result.Result.Content!; + IContent content = await EnsureOnlyAllowedFieldsAreUpdated(result.Result.Content!, userKey); ContentEditingOperationStatus updateTemplateStatus = await UpdateTemplateAsync(content, createModel.TemplateKey); if (updateTemplateStatus != ContentEditingOperationStatus.Success) { @@ -78,6 +122,53 @@ public async Task> C : Attempt.FailWithStatus(saveStatus, new ContentCreateResult { Content = content }); } + /// + /// A temporary method that ensures the data is sent in is overridden by the original data, in cases where the user do not have permissions to change the data. + /// + private async Task EnsureOnlyAllowedFieldsAreUpdated(IContent contentWithPotentialUnallowedChanges, Guid userKey) + { + if (contentWithPotentialUnallowedChanges.ContentType.VariesByCulture() is false) + { + return contentWithPotentialUnallowedChanges; + } + + IContent? existingContent = await GetAsync(contentWithPotentialUnallowedChanges.Key); + + IUser? user = await _userService.GetAsync(userKey); + + if (user is null) + { + return contentWithPotentialUnallowedChanges; + } + + var allowedLanguageIds = user.CalculateAllowedLanguageIds(_localizationService)!; + + var allowedCultures = (await _languageService.GetIsoCodesByIdsAsync(allowedLanguageIds)).ToHashSet(); + + foreach (var culture in contentWithPotentialUnallowedChanges.EditedCultures ?? contentWithPotentialUnallowedChanges.PublishedCultures) + { + if (allowedCultures.Contains(culture)) + { + continue; + } + + + // else override the updates values with the original values. + foreach (IProperty property in contentWithPotentialUnallowedChanges.Properties) + { + if (property.PropertyType.VariesByCulture() is false) + { + continue; + } + + var value = existingContent?.Properties.First(x=>x.Alias == property.Alias).GetValue(culture, null, false); + property.SetValue(value, culture, null); + } + } + + return contentWithPotentialUnallowedChanges; + } + public async Task> UpdateAsync(Guid key, ContentUpdateModel updateModel, Guid userKey) { IContent? content = ContentService.GetById(key); @@ -102,6 +193,8 @@ public async Task> U ContentEditingOperationStatus validationStatus = result.Status; ContentValidationResult validationResult = result.Result.ValidationResult; + content = await EnsureOnlyAllowedFieldsAreUpdated(content, userKey); + ContentEditingOperationStatus updateTemplateStatus = await UpdateTemplateAsync(content, updateModel.TemplateKey); if (updateTemplateStatus != ContentEditingOperationStatus.Success) {