diff --git a/src/Umbraco.Core/Constants-ModelStateErrorKeys.cs b/src/Umbraco.Core/Constants-ModelStateErrorKeys.cs new file mode 100644 index 0000000000..c5bee395aa --- /dev/null +++ b/src/Umbraco.Core/Constants-ModelStateErrorKeys.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Core; + +public static partial class Constants +{ + public class ModelStateErrorKeys + { + public const string PermissionError = "PermissionError"; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs index 1e548fdc30..1aba84b62c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -111,8 +111,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group) { return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, - group.StartContentId, group.StartMediaId, group.Alias, - Enumerable.Empty(), // TODO: Need to find the real languages when the dto model is updated + group.StartContentId, group.StartMediaId, group.Alias, group.UserGroup2LanguageDtos.Select(x => x.LanguageId), group.UserGroup2AppDtos.Select(x => x.AppAlias).WhereNotNull().ToArray(), group.DefaultPermissions == null ? Enumerable.Empty() : group.DefaultPermissions.ToCharArray().Select(x => x.ToString())); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 5f44dc6781..64b460fa22 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -393,6 +393,17 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 var startNodes = Database.Fetch(sql); + // get groups2languages + + sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserGroupId, groupIds); + + var groups2languages = Database.Fetch(sql) + .GroupBy(x => x.UserGroupId) + .ToDictionary(x => x.Key, x => x); + // map groups foreach (var user2group in users2groups) @@ -419,6 +430,16 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 if (groups2apps.TryGetValue(group.Id, out var list)) group.UserGroup2AppDtos = list.ToList(); // groups2apps is distinct } + + // map languages + + foreach (var group in groups.Values) + { + if (groups2languages.TryGetValue(group.Id, out var list)) + { + group.UserGroup2LanguageDtos = list.ToList(); // groups2apps is distinct + } + } } #endregion diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index e1374ea56d..80ac9113c4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -796,6 +796,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // we will continue to save if model state is invalid, however we cannot save if critical data is missing. if (!ModelState.IsValid) { + // Don't try and save if we do not have access + if (ModelState.Keys.Contains(Constants.ModelStateErrorKeys.PermissionError)) + { + var forDisplay = mapToDisplay(contentItem.PersistedContent); + return ValidationProblem(forDisplay, ModelState); + } + // check for critical data validation issues, we can't continue saving if this data is invalid if (!passesCriticalValidationRules) { diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index 430d38022a..aea64f1f11 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -10,6 +10,8 @@ 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.Membership; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.BackOffice.Authorization; using Umbraco.Cms.Web.Common.Authorization; @@ -33,6 +35,8 @@ namespace Umbraco.Cms.Web.BackOffice.Filters private readonly IContentService _contentService; private readonly IPropertyValidationService _propertyValidationService; private readonly IAuthorizationService _authorizationService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly ILocalizationService _localizationService; private readonly ILoggerFactory _loggerFactory; @@ -40,12 +44,16 @@ namespace Umbraco.Cms.Web.BackOffice.Filters ILoggerFactory loggerFactory, IContentService contentService, IPropertyValidationService propertyValidationService, - IAuthorizationService authorizationService) + IAuthorizationService authorizationService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + ILocalizationService localizationService) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); _propertyValidationService = propertyValidationService ?? throw new ArgumentNullException(nameof(propertyValidationService)); _authorizationService = authorizationService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _localizationService = localizationService; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) @@ -76,7 +84,7 @@ namespace Umbraco.Cms.Web.BackOffice.Filters if (!ValidateAtLeastOneVariantIsBeingSaved(model, context)) return; if (!contentItemValidator.ValidateExistingContent(model, context)) return; - if (!await ValidateUserAccessAsync(model, context)) return; + if (!await ValidateUserAccessAsync(model, context)) if (model is not null) { @@ -129,6 +137,31 @@ namespace Umbraco.Cms.Web.BackOffice.Filters var permissionToCheck = new List(); IContent? contentToCheck = null; int contentIdToCheck; + + // First check if user has Access to that language + IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + bool hasAccess = false; + if (currentUser is null) + { + return false; + } + + foreach (IReadOnlyUserGroup group in currentUser.Groups) + { + IEnumerable languages = _localizationService.GetAllLanguages().Where(x => group.AllowedLanguages.Contains(x.Id)); + if (group.AllowedLanguages.Count() is 0 || + languages.Select(x => x.IsoCode).Intersect(contentItem?.Variants.Where(x => x.Save || x.Publish).Select(x => x.Culture) ?? Enumerable.Empty()).Count() is not 0) + { + hasAccess = true; + } + } + + if (!hasAccess && contentItem?.Variants.First().Culture is not null) + { + actionContext.ModelState.AddModelError(Constants.ModelStateErrorKeys.PermissionError, "User does not have access to save language"); + return false; + } + switch (contentItem?.Action) { case ContentSaveAction.Save: