From 2494d8c5aa0f8832383524ebec857e34adef6818 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 27 Feb 2024 20:57:02 +0000 Subject: [PATCH] Granular permissions in Management API (#15734) * It builds.. * Added granular permissions * Added granular permissions * Rename content to document * Added migration * Fixed issues causing the migration from v13 was not possible. * Merged Permissions and Granular Permissions in viewmodel * Prepared the viewmodel to a future where permissions can be more types. * OpenApi * Allow to translate a single char to many strings * Use frontend friendly values for known permissions * Validate the documents exist * Allow setting non-document settings * Add "$type" when required * Rename to presentation model and update OpenApi.json * OpenApi.json * Fix tests * OpenAPI * Fixed issues with upgrades * Add the discriminator name * Fixed issues that only happended on SqlServer * Fixed queries for SqlServer * Clean up * More cleanup * Fix issue when migrating sqlserver * Split fallback permissions into own concept in view model * Also split on current user * Added a extenable pattern for mappers between DTO => Granular Permission => ViewModel and ViewModel => Granular Permission * Fixed issue with new exists method, that did not take duplicate keys into account. * Added sections to current user response model * Formatting fixes * Move class to its own file * xml comment --------- Co-authored-by: Zeegaan --- ...reUmbracoManagementApiSwaggerGenOptions.cs | 5 + .../Controllers/Media/ByKeyMediaController.cs | 1 - .../UserGroup/UpdateUserGroupController.cs | 4 +- .../UserGroup/UserGroupsControllerBase.cs | 4 + .../UserGroupsBuilderExtensions.cs | 9 + .../IPermissionPresentationFactory.cs | 14 + .../PermissionPresentationFactory.cs | 86 ++++++ .../Factories/UserGroupPresentationFactory.cs | 20 +- .../Factories/UserPresentationFactory.cs | 9 +- ...AuthenticationTokensNotificationHandler.cs | 88 +++--- .../Permissions/DocumentPermissionMapper.cs | 57 ++++ .../IPermissionPresentationMapper.cs | 15 + ...ceAuthorizationInitializationMiddleware.cs | 4 +- src/Umbraco.Cms.Api.Management/OpenApi.json | 105 ++++++- .../OpenApi/IOpenApiDiscriminator.cs | 11 + .../Content/ContentPermissionAuthorizer.cs | 8 +- .../Content/ContentPermissionResource.cs | 54 ++-- .../Content/IContentPermissionAuthorizer.cs | 24 +- .../Security/BackOfficeApplicationManager.cs | 4 +- .../User/Current/CurrentUserResponseModel.cs | 8 +- .../User/Current/UserPermissionViewModel.cs | 2 +- .../DocumentPermissionViewModel.cs | 7 + .../IPermissionPresentationModel.cs | 8 + .../UnknownTypePermissionViewModel.cs | 8 + .../ViewModels/UserGroup/UserGroupBase.cs | 9 +- .../Actions/ActionAssignDomain.cs | 4 +- src/Umbraco.Core/Actions/ActionBrowse.cs | 4 +- src/Umbraco.Core/Actions/ActionCollection.cs | 14 +- src/Umbraco.Core/Actions/ActionCopy.cs | 4 +- .../ActionCreateBlueprintFromContent.cs | 4 +- src/Umbraco.Core/Actions/ActionDelete.cs | 4 +- src/Umbraco.Core/Actions/ActionMove.cs | 4 +- src/Umbraco.Core/Actions/ActionNew.cs | 4 +- src/Umbraco.Core/Actions/ActionNotify.cs | 4 +- src/Umbraco.Core/Actions/ActionProtect.cs | 4 +- src/Umbraco.Core/Actions/ActionPublish.cs | 4 +- src/Umbraco.Core/Actions/ActionRestore.cs | 4 +- src/Umbraco.Core/Actions/ActionRights.cs | 4 +- src/Umbraco.Core/Actions/ActionRollback.cs | 4 +- src/Umbraco.Core/Actions/ActionSort.cs | 4 +- src/Umbraco.Core/Actions/ActionToPublish.cs | 4 +- src/Umbraco.Core/Actions/ActionUnpublish.cs | 4 +- src/Umbraco.Core/Actions/ActionUpdate.cs | 4 +- src/Umbraco.Core/Actions/IAction.cs | 6 +- .../Handlers/AuditNotificationsHandler.cs | 6 +- .../Models/ContentEditing/UserGroupSave.cs | 14 +- .../Models/Mapping/UserMapDefinition.cs | 6 +- .../Models/Membership/EntityPermission.cs | 6 +- .../Membership/EntityPermissionCollection.cs | 16 +- .../Models/Membership/EntityPermissionSet.cs | 2 +- .../Models/Membership/IReadOnlyUserGroup.cs | 15 +- .../Models/Membership/IUserGroup.cs | 18 +- .../Permissions/DocumentGranularPermission.cs | 37 +++ .../Permissions/IGranularPermission.cs | 10 + .../Permissions/INodeGranularPermission.cs | 11 + .../UnknownTypeGranularPermission.cs | 32 ++ .../Models/Membership/ReadOnlyUserGroup.cs | 25 +- .../Models/Membership/UserGroup.cs | 37 +-- .../Models/Membership/UserGroupExtensions.cs | 2 +- src/Umbraco.Core/Models/NodePermissions.cs | 2 +- .../Persistence/Constants-DatabaseSchema.cs | 7 +- .../Repositories/IDocumentRepository.cs | 6 +- .../Repositories/IEntityRepository.cs | 2 + .../Repositories/IUserGroupRepository.cs | 4 +- .../Security/ContentPermissions.cs | 16 +- .../Services/ContentPermissionService.cs | 12 +- src/Umbraco.Core/Services/ContentService.cs | 2 +- src/Umbraco.Core/Services/EntityService.cs | 8 + .../Services/IContentPermissionService.cs | 24 +- src/Umbraco.Core/Services/IContentService.cs | 2 +- src/Umbraco.Core/Services/IEntityService.cs | 6 + src/Umbraco.Core/Services/IUserService.cs | 4 +- .../UserGroupOperationStatus.cs | 1 + src/Umbraco.Core/Services/UserGroupService.cs | 26 ++ src/Umbraco.Core/Services/UserService.cs | 26 +- .../Services/UserServiceExtensions.cs | 5 +- .../Migrations/Install/DatabaseDataCreator.cs | 40 ++- .../Install/DatabaseSchemaCreator.cs | 3 +- .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../Upgrade/V_14_0_0/AddGuidsToUserGroups.cs | 3 + .../Upgrade/V_14_0_0/AddGuidsToUsers.cs | 2 +- .../MigrateCharPermissionsToStrings.cs | 114 +++++++ .../Dtos/UserGroup2GranularPermissionDto.cs | 45 +++ .../Persistence/Dtos/UserGroup2NodeDto.cs | 1 + .../Dtos/UserGroup2NodePermissionDto.cs | 1 + .../Dtos/UserGroup2PermissionDto.cs | 7 +- .../Persistence/Dtos/UserGroupDto.cs | 6 + .../Persistence/Factories/UserFactory.cs | 29 +- .../Persistence/Factories/UserGroupFactory.cs | 44 ++- .../Persistence/Mappers/IPermissionMapper.cs | 10 + .../Implement/ContentTypeRepositoryBase.cs | 4 +- .../Implement/DataTypeRepository.cs | 2 +- .../Implement/DocumentRepository.cs | 5 +- .../Implement/EntityRepository.cs | 7 + .../Repositories/Implement/MediaRepository.cs | 3 +- .../Implement/MemberGroupRepository.cs | 10 +- .../Implement/MemberRepository.cs | 5 +- .../Implement/PermissionRepository.cs | 277 ++++++++++-------- .../Implement/TemplateRepository.cs | 2 - .../Implement/UserGroupRepository.cs | 107 +++++-- .../Repositories/Implement/UserRepository.cs | 106 +++++-- .../Security/BackOfficeUserStore.cs | 1 + .../Builders/UserGroupBuilder.cs | 19 +- .../Mapping/UserModelMapperTests.cs | 6 +- .../Services/ContentServiceTests.cs | 8 +- .../Repositories/UserGroupRepositoryTest.cs | 10 +- .../Repositories/UserRepositoryTest.cs | 9 +- .../Services/UserServiceTests.cs | 48 +-- .../Security/ContentPermissionsTests.cs | 28 +- .../UserEditorAuthorizationHelperTests.cs | 3 +- .../Builders/UserGroupBuilderTests.cs | 4 +- 111 files changed, 1468 insertions(+), 544 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Factories/IPermissionPresentationFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/PermissionPresentationFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPermissionMapper.cs create mode 100644 src/Umbraco.Cms.Api.Management/Mapping/Permissions/IPermissionPresentationMapper.cs create mode 100644 src/Umbraco.Cms.Api.Management/OpenApi/IOpenApiDiscriminator.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/DocumentPermissionViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/IPermissionPresentationModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/UnknownTypePermissionViewModel.cs create mode 100644 src/Umbraco.Core/Models/Membership/Permissions/DocumentGranularPermission.cs create mode 100644 src/Umbraco.Core/Models/Membership/Permissions/IGranularPermission.cs create mode 100644 src/Umbraco.Core/Models/Membership/Permissions/INodeGranularPermission.cs create mode 100644 src/Umbraco.Core/Models/Membership/Permissions/UnknownTypeGranularPermission.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateCharPermissionsToStrings.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2GranularPermissionDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Mappers/IPermissionMapper.cs diff --git a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs index 9e78c5a471..d636b05a56 100644 --- a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs +++ b/src/Umbraco.Cms.Api.Management/Configuration/ConfigureUmbracoManagementApiSwaggerGenOptions.cs @@ -34,6 +34,11 @@ public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions< swaggerGenOptions.UseOneOfForPolymorphism(); swaggerGenOptions.UseAllOfForInheritance(); + // Ensure all types that implements the IOpenApiDiscriminator have a $type property in the OpenApi schema with the default value (The class name) that is expected by the server + swaggerGenOptions.SelectDiscriminatorNameUsing(type => typeof(IOpenApiDiscriminator).IsAssignableFrom(type) ? "$type" : null); + swaggerGenOptions.SelectDiscriminatorValueUsing(type => typeof(IOpenApiDiscriminator).IsAssignableFrom(type) ? type.Name : null); + + swaggerGenOptions.AddSecurityDefinition( ManagementApiConfiguration.ApiSecurityName, new OpenApiSecurityScheme diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/ByKeyMediaController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/ByKeyMediaController.cs index 38dc50e470..588554bccc 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/ByKeyMediaController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/ByKeyMediaController.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Security.Authorization.Media; -using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UpdateUserGroupController.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UpdateUserGroupController.cs index 8e3b6b9c28..5c080b43b6 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UpdateUserGroupController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UpdateUserGroupController.cs @@ -32,7 +32,7 @@ public class UpdateUserGroupController : UserGroupControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] - public async Task Update(Guid id, UpdateUserGroupRequestModel dataTypeRequestModel) + public async Task Update(Guid id, UpdateUserGroupRequestModel updateUserGroupRequestModel) { IUserGroup? existingUserGroup = await _userGroupService.GetAsync(id); @@ -41,7 +41,7 @@ public class UpdateUserGroupController : UserGroupControllerBase return UserGroupOperationStatusResult(UserGroupOperationStatus.NotFound); } - Attempt userGroupUpdateAttempt = await _userGroupPresentationFactory.UpdateAsync(existingUserGroup, dataTypeRequestModel); + Attempt userGroupUpdateAttempt = await _userGroupPresentationFactory.UpdateAsync(existingUserGroup, updateUserGroupRequestModel); if (userGroupUpdateAttempt.Success is false) { return UserGroupOperationStatusResult(userGroupUpdateAttempt.Status); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupsControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupsControllerBase.cs index c688114d60..6627e7f8e0 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupsControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupsControllerBase.cs @@ -50,6 +50,10 @@ public class UserGroupControllerBase : ManagementApiControllerBase .WithTitle("Media start node key not found") .WithDetail("The assigned media start node does not exists.") .Build()), + UserGroupOperationStatus.DocumentPermissionKeyNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("A document permission key not found") + .WithDetail("A assigned document permission not exists.") + .Build()), UserGroupOperationStatus.LanguageNotFound => NotFound(problemDetailsBuilder .WithTitle("Language not found") .WithDetail("The specified language cannot be found.") diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UserGroupsBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UserGroupsBuilderExtensions.cs index ce449412cc..b7902e490a 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UserGroupsBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UserGroupsBuilderExtensions.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Mapping.Permissions; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure.Persistence.Mappers; namespace Umbraco.Cms.Api.Management.DependencyInjection; @@ -9,6 +11,13 @@ internal static class UserGroupsBuilderExtensions internal static IUmbracoBuilder AddUserGroups(this IUmbracoBuilder builder) { builder.Services.AddTransient(); + builder.Services.AddSingleton(); + + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(x=>x.GetRequiredService()); + builder.Services.AddSingleton(x=>x.GetRequiredService()); + return builder; } } diff --git a/src/Umbraco.Cms.Api.Management/Factories/IPermissionPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IPermissionPresentationFactory.cs new file mode 100644 index 0000000000..b261ce01a4 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IPermissionPresentationFactory.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Api.Management.ViewModels.UserGroup; +using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; +using Umbraco.Cms.Core.Models.Membership.Permissions; + +namespace Umbraco.Cms.Api.Management.Factories; + +/// +/// A factory for creating +/// +public interface IPermissionPresentationFactory +{ + Task> CreateAsync(ISet userGroupGranularPermissions); + Task> CreatePermissionSetsAsync(ISet permissions); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/PermissionPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/PermissionPresentationFactory.cs new file mode 100644 index 0000000000..e3189b2d7a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/PermissionPresentationFactory.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Api.Management.Mapping.Permissions; +using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; +using Umbraco.Cms.Core.Models.Membership.Permissions; + +namespace Umbraco.Cms.Api.Management.Factories; + +public class PermissionPresentationFactory : IPermissionPresentationFactory +{ + private readonly ILogger _logger; + private readonly IDictionary _permissionPresentationMappersByContext; + private readonly Dictionary _permissionPresentationMappersByType; + + public PermissionPresentationFactory(IEnumerable permissionPresentationMappers, ILogger logger) + { + _logger = logger; + _permissionPresentationMappersByContext = permissionPresentationMappers.ToDictionary(x => x.Context); + _permissionPresentationMappersByType = permissionPresentationMappers.ToDictionary(x => x.PresentationModelToHandle); + } + + public Task> CreateAsync(ISet granularPermissions) + { + var result = new HashSet(); + + IEnumerable> contexts = granularPermissions.GroupBy(x => x.Context); + + foreach (IGrouping contextGroup in contexts) + { + if (_permissionPresentationMappersByContext.TryGetValue(contextGroup.Key, out IPermissionPresentationMapper? mapper)) + { + IEnumerable mapped = mapper.MapManyAsync(contextGroup); + foreach (IPermissionPresentationModel permissionPresentationModel in mapped) + { + result.Add(permissionPresentationModel); + } + } + else + { + IEnumerable> keyGroups = contextGroup.GroupBy(x => x.Key); + foreach (IGrouping keyGroup in keyGroups) + { + var verbs = keyGroup.Select(x => x.Permission).ToHashSet(); + result.Add(new UnknownTypePermissionPresentationModel() + { + Context = contextGroup.Key, Verbs = verbs + }); + } + } + } + + return Task.FromResult>(result); + } + + public Task> CreatePermissionSetsAsync(ISet permissions) + { + ISet granularPermissions = new HashSet(); + foreach (IPermissionPresentationModel permissionViewModel in permissions) + { + if (_permissionPresentationMappersByType.TryGetValue(permissionViewModel.GetType(), out IPermissionPresentationMapper? mapper)) + { + IEnumerable mapped = mapper.MapToGranularPermissions(permissionViewModel); + foreach (IGranularPermission granularPermission in mapped) + { + granularPermissions.Add(granularPermission); + } + } + else if (permissionViewModel is UnknownTypePermissionPresentationModel unknownTypePermissionViewModel) + { + foreach (var verb in unknownTypePermissionViewModel.Verbs) + { + granularPermissions.Add(new UnknownTypeGranularPermission + { + Context = unknownTypePermissionViewModel.Context, + Permission = verb + }); + } + } + else + { + _logger.LogWarning("Unknown mapper for type {Type} to IGranularPermission", permissionViewModel.GetType()); + } + } + + return Task.FromResult(granularPermissions); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs index 3009be98f3..d05ff8aa52 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs @@ -1,9 +1,11 @@ using Umbraco.Cms.Api.Management.Mapping; using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.UserGroup; +using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Core.Strings; @@ -17,15 +19,18 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory private readonly IEntityService _entityService; private readonly IShortStringHelper _shortStringHelper; private readonly ILanguageService _languageService; + private readonly IPermissionPresentationFactory _permissionPresentationFactory; public UserGroupPresentationFactory( IEntityService entityService, IShortStringHelper shortStringHelper, - ILanguageService languageService) + ILanguageService languageService, + IPermissionPresentationFactory permissionPresentationFactory) { _entityService = entityService; _shortStringHelper = shortStringHelper; _languageService = languageService; + _permissionPresentationFactory = permissionPresentationFactory; } /// @@ -55,11 +60,13 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory Icon = userGroup.Icon, Languages = languageIsoCodesMappingAttempt.Result, HasAccessToAllLanguages = userGroup.HasAccessToAllLanguages, - Permissions = userGroup.PermissionNames, + FallbackPermissions = userGroup.Permissions, + Permissions = await _permissionPresentationFactory.CreateAsync(userGroup.GranularPermissions), Sections = userGroup.AllowedSections.Select(SectionMapper.GetName), IsSystemGroup = userGroup.IsSystemUserGroup() }; } + /// public async Task CreateAsync(IReadOnlyUserGroup userGroup) { @@ -83,7 +90,8 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory Icon = userGroup.Icon, Languages = languageIsoCodesMappingAttempt.Result, HasAccessToAllLanguages = userGroup.HasAccessToAllLanguages, - Permissions = userGroup.PermissionNames, + FallbackPermissions = userGroup.Permissions, + Permissions = await _permissionPresentationFactory.CreateAsync(userGroup.GranularPermissions), Sections = userGroup.AllowedSections.Select(SectionMapper.GetName), }; } @@ -122,7 +130,8 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory Alias = cleanedName, Icon = requestModel.Icon, HasAccessToAllLanguages = requestModel.HasAccessToAllLanguages, - PermissionNames = requestModel.Permissions, + Permissions = requestModel.FallbackPermissions, + GranularPermissions = await _permissionPresentationFactory.CreatePermissionSetsAsync(requestModel.Permissions) }; Attempt assignmentAttempt = AssignStartNodesToUserGroup(requestModel, group); @@ -180,8 +189,9 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory current.Name = request.Name.CleanForXss('[', ']', '(', ')', ':'); current.Icon = request.Icon; current.HasAccessToAllLanguages = request.HasAccessToAllLanguages; - current.PermissionNames = request.Permissions; + current.Permissions = request.FallbackPermissions; + current.GranularPermissions = await _permissionPresentationFactory.CreatePermissionSetsAsync(request.Permissions); return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, current); } diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs index a23eeab7b6..80cc6a3372 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs @@ -157,9 +157,12 @@ public class UserPresentationFactory : IUserPresentationFactory var mediaStartNodeKeys = GetKeysFromIds(user.CalculateMediaStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Media); var documentStartNodeKeys = GetKeysFromIds(user.CalculateContentStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Document); - var permissions = presentationGroups.SelectMany(x => x.Permissions).Distinct().ToHashSet(); + var permissions = presentationGroups.SelectMany(x => x.Permissions).ToHashSet(); + var fallbackPermissions = presentationGroups.SelectMany(x => x.FallbackPermissions).ToHashSet(); + var hasAccessToAllLanguages = presentationGroups.Any(x => x.HasAccessToAllLanguages); + var allowedSections = presentationGroups.SelectMany(x => x.Sections).ToHashSet(); return await Task.FromResult(new CurrentUserResponseModel() { Id = presentationUser.Id, @@ -172,7 +175,9 @@ public class UserPresentationFactory : IUserPresentationFactory MediaStartNodeIds = mediaStartNodeKeys, DocumentStartNodeIds = documentStartNodeKeys, Permissions = permissions, - HasAccessToAllLanguages = hasAccessToAllLanguages + FallbackPermissions = fallbackPermissions, + HasAccessToAllLanguages = hasAccessToAllLanguages, + AllowedSections = allowedSections }); } diff --git a/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs b/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs index 5749dce5b4..a418f3caed 100644 --- a/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs +++ b/src/Umbraco.Cms.Api.Management/Handlers/RevokeUserAuthenticationTokensNotificationHandler.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using Microsoft.Extensions.Logging; using OpenIddict.Abstractions; using Umbraco.Cms.Core.Events; @@ -37,66 +38,81 @@ internal sealed class RevokeUserAuthenticationTokensNotificationHandler : // We need to know the pre-saving state of the saved users in order to compare if their access has changed public async Task HandleAsync(UserSavingNotification notification, CancellationToken cancellationToken) { - var usersAccess = new Dictionary(); - foreach (IUser user in notification.SavedEntities) + try { - UserStartNodesAndGroupAccess? priorUserAccess = await GetRelevantUserAccessDataByUserKeyAsync(user.Key); - if (priorUserAccess == null) + var usersAccess = new Dictionary(); + foreach (IUser user in notification.SavedEntities) { - continue; + UserStartNodesAndGroupAccess? priorUserAccess = await GetRelevantUserAccessDataByUserKeyAsync(user.Key); + if (priorUserAccess == null) + { + continue; + } + + usersAccess.Add(user.Key, priorUserAccess); } - usersAccess.Add(user.Key, priorUserAccess); + notification.State[NotificationStateKey] = usersAccess; + } + catch (DbException e) + { + _logger.LogWarning(e, "This is expected when we upgrade from < Umbraco 14. Otherwise it should not happen"); } - - notification.State[NotificationStateKey] = usersAccess; } public async Task HandleAsync(UserSavedNotification notification, CancellationToken cancellationToken) { - Dictionary? preSavingUsersState = null; - - if (notification.State.TryGetValue(NotificationStateKey, out var value)) + try { - preSavingUsersState = value as Dictionary; - } + Dictionary? preSavingUsersState = null; - // If we have a new user, there is no token - if (preSavingUsersState is null || preSavingUsersState.Count == 0) - { - return; - } - - foreach (IUser user in notification.SavedEntities) - { - if (user.IsSuper()) + if (notification.State.TryGetValue(NotificationStateKey, out var value)) { - continue; + preSavingUsersState = value as Dictionary; } - // When a user is locked out and/or un-approved, make sure we revoke all tokens - if (user.IsLockedOut || user.IsApproved is false) + // If we have a new user, there is no token + if (preSavingUsersState is null || preSavingUsersState.Count == 0) { - await RevokeTokensAsync(user); - continue; + return; } - // Don't revoke admin tokens to prevent log out when accidental changes - if (user.IsAdmin()) + foreach (IUser user in notification.SavedEntities) { - continue; - } + if (user.IsSuper()) + { + continue; + } - // Check if the user access has changed - we also need to revoke all tokens in this case - if (preSavingUsersState.TryGetValue(user.Key, out UserStartNodesAndGroupAccess? preSavingState)) - { - UserStartNodesAndGroupAccess postSavingState = MapToUserStartNodesAndGroupAccess(user); - if (preSavingState.CompareAccess(postSavingState) == false) + // When a user is locked out and/or un-approved, make sure we revoke all tokens + if (user.IsLockedOut || user.IsApproved is false) { await RevokeTokensAsync(user); + continue; + } + + // Don't revoke admin tokens to prevent log out when accidental changes + if (user.IsAdmin()) + { + continue; + } + + // Check if the user access has changed - we also need to revoke all tokens in this case + if (preSavingUsersState.TryGetValue(user.Key, out UserStartNodesAndGroupAccess? preSavingState)) + { + UserStartNodesAndGroupAccess postSavingState = MapToUserStartNodesAndGroupAccess(user); + if (preSavingState.CompareAccess(postSavingState) == false) + { + await RevokeTokensAsync(user); + } } } } + catch (DbException e) + { + _logger.LogWarning(e, "This is expected when we upgrade from < Umbraco 14. Otherwise it should not happen"); + } + } // We can only delete non-logged in users in Umbraco, meaning that such will not have a token, diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPermissionMapper.cs b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPermissionMapper.cs new file mode 100644 index 0000000000..afdbd0f618 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPermissionMapper.cs @@ -0,0 +1,57 @@ +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; +using Umbraco.Cms.Core.Models.Membership.Permissions; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Mappers; + +namespace Umbraco.Cms.Api.Management.Mapping.Permissions; +/// +/// Mapping required for mapping all the way from viewmodel to database and back. +/// +/// +/// This mapping maps all the way from management api to database in one file intentionally, so it is very clear what it takes, if we wanna add permissions to media or other types in the future. +/// +public class DocumentPermissionMapper : IPermissionPresentationMapper, IPermissionMapper +{ + public string Context => DocumentGranularPermission.ContextType; + public IGranularPermission MapFromDto(UserGroup2GranularPermissionDto dto) => + new DocumentGranularPermission() + { + Key = dto.UniqueId!.Value, + Permission = dto.Permission, + }; + + public Type PresentationModelToHandle => typeof(DocumentPermissionPresentationModel); + + public IEnumerable MapManyAsync(IEnumerable granularPermissions) + { + IEnumerable> keyGroups = granularPermissions.GroupBy(x => x.Key); + foreach (IGrouping keyGroup in keyGroups) + { + var verbs = keyGroup.Select(x => x.Permission).ToHashSet(); + if (keyGroup.Key.HasValue) + { + yield return new DocumentPermissionPresentationModel + { + Document = new ReferenceByIdModel(keyGroup.Key.Value), + Verbs = verbs, + }; + } + } + } + + public IEnumerable MapToGranularPermissions(IPermissionPresentationModel permissionViewModel) + { + if (permissionViewModel is DocumentPermissionPresentationModel documentPermissionPresentationModel) + { + foreach (var verb in documentPermissionPresentationModel.Verbs) + { + yield return new DocumentGranularPermission + { + Key = documentPermissionPresentationModel.Document.Id, + Permission = verb, + }; + } + } + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Permissions/IPermissionPresentationMapper.cs b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/IPermissionPresentationMapper.cs new file mode 100644 index 0000000000..8ba261ce6e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/IPermissionPresentationMapper.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; +using Umbraco.Cms.Core.Models.Membership.Permissions; + +namespace Umbraco.Cms.Api.Management.Mapping.Permissions; + +public interface IPermissionPresentationMapper +{ + string Context { get; } + + Type PresentationModelToHandle { get; } + + IEnumerable MapManyAsync(IEnumerable granularPermissions); + + IEnumerable MapToGranularPermissions(IPermissionPresentationModel permissionViewModel); +} diff --git a/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs b/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs index 6a47d378dc..c9179215df 100644 --- a/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs +++ b/src/Umbraco.Cms.Api.Management/Middleware/BackOfficeAuthorizationInitializationMiddleware.cs @@ -40,7 +40,9 @@ public class BackOfficeAuthorizationInitializationMiddleware : IMiddleware return; } - if (_runtimeState.Level < RuntimeLevel.Run) + // Install is okay without this, because we do not need a token to install, + // but upgrades do, so we need to execute for everything higher then or equal to upgrade. + if (_runtimeState.Level < RuntimeLevel.Upgrade) { return; } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 253ade9adf..35d05f79fc 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -27163,9 +27163,11 @@ }, "CurrentUserResponseModel": { "required": [ + "allowedSections", "avatarUrls", "documentStartNodeIds", "email", + "fallbackPermissions", "hasAccessToAllLanguages", "id", "languages", @@ -27224,7 +27226,28 @@ "hasAccessToAllLanguages": { "type": "boolean" }, + "fallbackPermissions": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, "permissions": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentPermissionPresentationModel" + }, + { + "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" + } + ] + } + }, + "allowedSections": { "uniqueItems": true, "type": "array", "items": { @@ -27829,6 +27852,40 @@ }, "additionalProperties": false }, + "DocumentPermissionPresentationModel": { + "required": [ + "$type", + "document", + "verbs" + ], + "type": "object", + "properties": { + "$type": { + "type": "string" + }, + "document": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + }, + "verbs": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "DocumentPermissionPresentationModel": "#/components/schemas/DocumentPermissionPresentationModel" + } + } + }, "DocumentRecycleBinItemResponseModel": { "required": [ "documentType", @@ -32960,6 +33017,36 @@ }, "additionalProperties": false }, + "UnknownTypePermissionPresentationModel": { + "required": [ + "$type", + "context", + "verbs" + ], + "type": "object", + "properties": { + "$type": { + "type": "string" + }, + "verbs": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "context": { + "type": "string" + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "UnknownTypePermissionPresentationModel": "#/components/schemas/UnknownTypePermissionPresentationModel" + } + } + }, "UnlockUsersRequestModel": { "required": [ "userIds" @@ -33815,6 +33902,7 @@ "UserGroupBaseModel": { "required": [ "documentRootAccess", + "fallbackPermissions", "hasAccessToAllLanguages", "languages", "mediaRootAccess", @@ -33868,12 +33956,26 @@ "mediaRootAccess": { "type": "boolean" }, - "permissions": { + "fallbackPermissions": { "uniqueItems": true, "type": "array", "items": { "type": "string" } + }, + "permissions": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentPermissionPresentationModel" + }, + { + "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" + } + ] + } } }, "additionalProperties": false @@ -33981,6 +34083,7 @@ "format": "uuid" }, "permissions": { + "uniqueItems": true, "type": "array", "items": { "type": "string" diff --git a/src/Umbraco.Cms.Api.Management/OpenApi/IOpenApiDiscriminator.cs b/src/Umbraco.Cms.Api.Management/OpenApi/IOpenApiDiscriminator.cs new file mode 100644 index 0000000000..6bf39b6c92 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/OpenApi/IOpenApiDiscriminator.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Api.Management.OpenApi; + +/// +/// Marker interface that ensure the type have a "$type" discriminator in the open api schema. +/// +/// +/// This is required when an endpoint can receive different types, to ensure the correct type is deserialized. +/// +public interface IOpenApiDiscriminator +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionAuthorizer.cs index 8e4a2ff5a8..5a7e55d153 100644 --- a/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionAuthorizer.cs +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionAuthorizer.cs @@ -18,7 +18,7 @@ internal sealed class ContentPermissionAuthorizer : IContentPermissionAuthorizer } /// - public async Task IsDeniedAsync(IPrincipal currentUser, IEnumerable contentKeys, ISet permissionsToCheck) + public async Task IsDeniedAsync(IPrincipal currentUser, IEnumerable contentKeys, ISet permissionsToCheck) { if (!contentKeys.Any()) { @@ -35,7 +35,7 @@ internal sealed class ContentPermissionAuthorizer : IContentPermissionAuthorizer } /// - public async Task IsDeniedWithDescendantsAsync(IPrincipal currentUser, Guid parentKey, ISet permissionsToCheck) + public async Task IsDeniedWithDescendantsAsync(IPrincipal currentUser, Guid parentKey, ISet permissionsToCheck) { IUser user = _authorizationHelper.GetUmbracoUser(currentUser); @@ -46,7 +46,7 @@ internal sealed class ContentPermissionAuthorizer : IContentPermissionAuthorizer } /// - public async Task IsDeniedAtRootLevelAsync(IPrincipal currentUser, ISet permissionsToCheck) + public async Task IsDeniedAtRootLevelAsync(IPrincipal currentUser, ISet permissionsToCheck) { IUser user = _authorizationHelper.GetUmbracoUser(currentUser); @@ -57,7 +57,7 @@ internal sealed class ContentPermissionAuthorizer : IContentPermissionAuthorizer } /// - public async Task IsDeniedAtRecycleBinLevelAsync(IPrincipal currentUser, ISet permissionsToCheck) + public async Task IsDeniedAtRecycleBinLevelAsync(IPrincipal currentUser, ISet permissionsToCheck) { IUser user = _authorizationHelper.GetUmbracoUser(currentUser); diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionResource.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionResource.cs index 921aba05ff..6ee9cf3529 100644 --- a/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionResource.cs +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/ContentPermissionResource.cs @@ -13,7 +13,7 @@ public class ContentPermissionResource : IPermissionResource /// The permission to check for. /// The key of the content or null if root. /// An instance of . - public static ContentPermissionResource WithKeys(char permissionToCheck, Guid? contentKey) => + public static ContentPermissionResource WithKeys(string permissionToCheck, Guid? contentKey) => contentKey is null ? Root(permissionToCheck) : WithKeys(permissionToCheck, contentKey.Value.Yield()); @@ -25,7 +25,7 @@ public class ContentPermissionResource : IPermissionResource /// The key of the content or null if root. /// The cultures to validate /// An instance of . - public static ContentPermissionResource WithKeys(char permissionToCheck, Guid? contentKey, IEnumerable cultures) => + public static ContentPermissionResource WithKeys(string permissionToCheck, Guid? contentKey, IEnumerable cultures) => contentKey is null ? Root(permissionToCheck, cultures) : WithKeys(permissionToCheck, contentKey.Value.Yield(), cultures); @@ -36,12 +36,12 @@ public class ContentPermissionResource : IPermissionResource /// The permission to check for. /// The keys of the contents or null if root. /// An instance of . - public static ContentPermissionResource WithKeys(char permissionToCheck, IEnumerable contentKeys) + public static ContentPermissionResource WithKeys(string permissionToCheck, IEnumerable contentKeys) { var hasRoot = contentKeys.Any(x => x is null); IEnumerable keys = contentKeys.Where(x => x.HasValue).Select(x => x!.Value); - return new ContentPermissionResource(keys, new HashSet { permissionToCheck }, hasRoot, false, null, null); + return new ContentPermissionResource(keys, new HashSet { permissionToCheck }, hasRoot, false, null, null); } /// @@ -50,7 +50,7 @@ public class ContentPermissionResource : IPermissionResource /// The permission to check for. /// The key of the content. /// An instance of . - public static ContentPermissionResource WithKeys(char permissionToCheck, Guid contentKey) => WithKeys(permissionToCheck, contentKey.Yield()); + public static ContentPermissionResource WithKeys(string permissionToCheck, Guid contentKey) => WithKeys(permissionToCheck, contentKey.Yield()); /// /// Creates a with the specified permission and content key. @@ -59,7 +59,7 @@ public class ContentPermissionResource : IPermissionResource /// The key of the content. /// The required culture access /// An instance of . - public static ContentPermissionResource WithKeys(char permissionToCheck, Guid contentKey,IEnumerable cultures) => WithKeys(permissionToCheck, contentKey.Yield(),cultures); + public static ContentPermissionResource WithKeys(string permissionToCheck, Guid contentKey,IEnumerable cultures) => WithKeys(permissionToCheck, contentKey.Yield(),cultures); /// /// Creates a with the specified permission and content keys. @@ -67,8 +67,8 @@ public class ContentPermissionResource : IPermissionResource /// The permission to check for. /// The keys of the contents. /// An instance of . - public static ContentPermissionResource WithKeys(char permissionToCheck, IEnumerable contentKeys) => - new ContentPermissionResource(contentKeys, new HashSet { permissionToCheck }, false, false, null, null); + public static ContentPermissionResource WithKeys(string permissionToCheck, IEnumerable contentKeys) => + new ContentPermissionResource(contentKeys, new HashSet { permissionToCheck }, false, false, null, null); /// /// Creates a with the specified permission and content keys. @@ -77,10 +77,10 @@ public class ContentPermissionResource : IPermissionResource /// The keys of the contents. /// The required culture access /// An instance of . - public static ContentPermissionResource WithKeys(char permissionToCheck, IEnumerable contentKeys, IEnumerable cultures) => + public static ContentPermissionResource WithKeys(string permissionToCheck, IEnumerable contentKeys, IEnumerable cultures) => new ContentPermissionResource( contentKeys, - new HashSet { permissionToCheck }, + new HashSet { permissionToCheck }, false, false, null, @@ -92,7 +92,7 @@ public class ContentPermissionResource : IPermissionResource /// The permissions to check for. /// The keys of the contents. /// An instance of . - public static ContentPermissionResource WithKeys(ISet permissionsToCheck, IEnumerable contentKeys) => + public static ContentPermissionResource WithKeys(ISet permissionsToCheck, IEnumerable contentKeys) => new ContentPermissionResource(contentKeys, permissionsToCheck, false, false, null, null); /// @@ -100,8 +100,8 @@ public class ContentPermissionResource : IPermissionResource /// /// The permission to check for. /// An instance of . - public static ContentPermissionResource Root(char permissionToCheck) => - new ContentPermissionResource(Enumerable.Empty(), new HashSet { permissionToCheck }, true, false, null, null); + public static ContentPermissionResource Root(string permissionToCheck) => + new ContentPermissionResource(Enumerable.Empty(), new HashSet { permissionToCheck }, true, false, null, null); /// /// Creates a with the specified permission and the root. @@ -109,15 +109,15 @@ public class ContentPermissionResource : IPermissionResource /// The permission to check for. /// The cultures to validate /// An instance of . - public static ContentPermissionResource Root(char permissionToCheck, IEnumerable cultures) => - new ContentPermissionResource(Enumerable.Empty(), new HashSet { permissionToCheck }, true, false, null, new HashSet(cultures)); + public static ContentPermissionResource Root(string permissionToCheck, IEnumerable cultures) => + new ContentPermissionResource(Enumerable.Empty(), new HashSet { permissionToCheck }, true, false, null, new HashSet(cultures)); /// /// Creates a with the specified permissions and the root. /// /// The permissions to check for. /// An instance of . - public static ContentPermissionResource Root(ISet permissionsToCheck) => + public static ContentPermissionResource Root(ISet permissionsToCheck) => new ContentPermissionResource(Enumerable.Empty(), permissionsToCheck, true, false, null, null); /// @@ -126,7 +126,7 @@ public class ContentPermissionResource : IPermissionResource /// The permissions to check for. /// The cultures to validate /// An instance of . - public static ContentPermissionResource Root(ISet permissionsToCheck, IEnumerable cultures) => + public static ContentPermissionResource Root(ISet permissionsToCheck, IEnumerable cultures) => new ContentPermissionResource(Enumerable.Empty(), permissionsToCheck, true, false, null, new HashSet(cultures)); @@ -136,7 +136,7 @@ public class ContentPermissionResource : IPermissionResource /// /// The permissions to check for. /// An instance of . - public static ContentPermissionResource RecycleBin(ISet permissionsToCheck) => + public static ContentPermissionResource RecycleBin(ISet permissionsToCheck) => new ContentPermissionResource(Enumerable.Empty(), permissionsToCheck, false, true, null, null); /// @@ -144,8 +144,8 @@ public class ContentPermissionResource : IPermissionResource /// /// The permission to check for. /// An instance of . - public static ContentPermissionResource RecycleBin(char permissionToCheck) => - new ContentPermissionResource(Enumerable.Empty(), new HashSet { permissionToCheck }, false, true, null, null); + public static ContentPermissionResource RecycleBin(string permissionToCheck) => + new ContentPermissionResource(Enumerable.Empty(), new HashSet { permissionToCheck }, false, true, null, null); /// /// Creates a with the specified permissions and the branch from the specified parent key. @@ -153,7 +153,7 @@ public class ContentPermissionResource : IPermissionResource /// The permissions to check for. /// The parent key of the branch. /// An instance of . - public static ContentPermissionResource Branch(ISet permissionsToCheck, Guid parentKeyForBranch) => + public static ContentPermissionResource Branch(ISet permissionsToCheck, Guid parentKeyForBranch) => new ContentPermissionResource(Enumerable.Empty(), permissionsToCheck, false, true, parentKeyForBranch, null); /// @@ -162,8 +162,8 @@ public class ContentPermissionResource : IPermissionResource /// The permission to check for. /// The parent key of the branch. /// An instance of . - public static ContentPermissionResource Branch(char permissionToCheck, Guid parentKeyForBranch) => - new ContentPermissionResource(Enumerable.Empty(), new HashSet { permissionToCheck }, false, true, parentKeyForBranch, null); + public static ContentPermissionResource Branch(string permissionToCheck, Guid parentKeyForBranch) => + new ContentPermissionResource(Enumerable.Empty(), new HashSet { permissionToCheck }, false, true, parentKeyForBranch, null); /// /// Creates a with the specified permission and the branch from the specified parent key. @@ -172,10 +172,10 @@ public class ContentPermissionResource : IPermissionResource /// The parent key of the branch. /// The required cultures /// An instance of . - public static ContentPermissionResource Branch(char permissionToCheck, Guid parentKeyForBranch, IEnumerable culturesToCheck) => + public static ContentPermissionResource Branch(string permissionToCheck, Guid parentKeyForBranch, IEnumerable culturesToCheck) => new ContentPermissionResource( Enumerable.Empty(), - new HashSet { permissionToCheck }, + new HashSet { permissionToCheck }, false, true, parentKeyForBranch, @@ -183,7 +183,7 @@ public class ContentPermissionResource : IPermissionResource private ContentPermissionResource( IEnumerable contentKeys, - ISet permissionsToCheck, + ISet permissionsToCheck, bool checkRoot, bool checkRecycleBin, Guid? parentKeyForBranch, ISet? culturesToCheck) @@ -207,7 +207,7 @@ public class ContentPermissionResource : IPermissionResource /// /// All permissions have to be satisfied when evaluating. /// - public ISet PermissionsToCheck { get; } + public ISet PermissionsToCheck { get; } /// /// Gets a value indicating whether to check for the root. diff --git a/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/IContentPermissionAuthorizer.cs b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/IContentPermissionAuthorizer.cs index bb9a5d7782..2e7acd2df2 100644 --- a/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/IContentPermissionAuthorizer.cs +++ b/src/Umbraco.Cms.Api.Management/Security/Authorization/Content/IContentPermissionAuthorizer.cs @@ -15,8 +15,8 @@ public interface IContentPermissionAuthorizer /// The key of the content item to check for. /// The permission to authorize. /// Returns true if authorization is successful, otherwise false. - Task IsAuthorizedAsync(IPrincipal currentUser, Guid contentKey, char permissionToCheck) - => IsDeniedAsync(currentUser, contentKey.Yield(), new HashSet { permissionToCheck }); + Task IsAuthorizedAsync(IPrincipal currentUser, Guid contentKey, string permissionToCheck) + => IsDeniedAsync(currentUser, contentKey.Yield(), new HashSet { permissionToCheck }); /// /// Authorizes whether the current user has access to the specified content item(s). @@ -25,7 +25,7 @@ public interface IContentPermissionAuthorizer /// The keys of the content items to check for. /// The collection of permissions to authorize. /// Returns true if authorization is successful, otherwise false. - Task IsDeniedAsync(IPrincipal currentUser, IEnumerable contentKeys, ISet permissionsToCheck); + Task IsDeniedAsync(IPrincipal currentUser, IEnumerable contentKeys, ISet permissionsToCheck); /// /// Authorizes whether the current user has access to the descendants of the specified content item. @@ -34,8 +34,8 @@ public interface IContentPermissionAuthorizer /// The key of the parent content item. /// The permission to authorize. /// Returns true if authorization is successful, otherwise false. - Task IsAuthorizedWithDescendantsAsync(IPrincipal currentUser, Guid parentKey, char permissionToCheck) - => IsDeniedWithDescendantsAsync(currentUser, parentKey, new HashSet { permissionToCheck }); + Task IsAuthorizedWithDescendantsAsync(IPrincipal currentUser, Guid parentKey, string permissionToCheck) + => IsDeniedWithDescendantsAsync(currentUser, parentKey, new HashSet { permissionToCheck }); /// /// Authorizes whether the current user has access to the descendants of the specified content item. @@ -44,7 +44,7 @@ public interface IContentPermissionAuthorizer /// The key of the parent content item. /// The collection of permissions to authorize. /// Returns true if authorization is successful, otherwise false. - Task IsDeniedWithDescendantsAsync(IPrincipal currentUser, Guid parentKey, ISet permissionsToCheck); + Task IsDeniedWithDescendantsAsync(IPrincipal currentUser, Guid parentKey, ISet permissionsToCheck); /// /// Authorizes whether the current user has access to the root item. @@ -52,8 +52,8 @@ public interface IContentPermissionAuthorizer /// The current user's principal. /// The permission to authorize. /// Returns true if authorization is successful, otherwise false. - Task IsAuthorizedAtRootLevelAsync(IPrincipal currentUser, char permissionToCheck) - => IsDeniedAtRootLevelAsync(currentUser, new HashSet { permissionToCheck }); + Task IsAuthorizedAtRootLevelAsync(IPrincipal currentUser, string permissionToCheck) + => IsDeniedAtRootLevelAsync(currentUser, new HashSet { permissionToCheck }); /// /// Authorizes whether the current user has access to the root item. @@ -61,7 +61,7 @@ public interface IContentPermissionAuthorizer /// The current user's principal. /// The collection of permissions to authorize. /// Returns true if authorization is successful, otherwise false. - Task IsDeniedAtRootLevelAsync(IPrincipal currentUser, ISet permissionsToCheck); + Task IsDeniedAtRootLevelAsync(IPrincipal currentUser, ISet permissionsToCheck); /// /// Authorizes whether the current user has access to the recycle bin item. @@ -69,8 +69,8 @@ public interface IContentPermissionAuthorizer /// The current user's principal. /// The permission to authorize. /// Returns true if authorization is successful, otherwise false. - Task IsAuthorizedAtRecycleBinLevelAsync(IPrincipal currentUser, char permissionToCheck) - => IsDeniedAtRecycleBinLevelAsync(currentUser, new HashSet { permissionToCheck }); + Task IsAuthorizedAtRecycleBinLevelAsync(IPrincipal currentUser, string permissionToCheck) + => IsDeniedAtRecycleBinLevelAsync(currentUser, new HashSet { permissionToCheck }); /// /// Authorizes whether the current user has access to the recycle bin item. @@ -78,7 +78,7 @@ public interface IContentPermissionAuthorizer /// The current user's principal. /// The collection of permissions to authorize. /// Returns true if authorization is successful, otherwise false. - Task IsDeniedAtRecycleBinLevelAsync(IPrincipal currentUser, ISet permissionsToCheck); + Task IsDeniedAtRecycleBinLevelAsync(IPrincipal currentUser, ISet permissionsToCheck); Task IsDeniedForCultures(IPrincipal currentUser, ISet culturesToCheck); } diff --git a/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs b/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs index a4d12931ae..972f4b40c8 100644 --- a/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs +++ b/src/Umbraco.Cms.Api.Management/Security/BackOfficeApplicationManager.cs @@ -31,7 +31,9 @@ public class BackOfficeApplicationManager : OpenIdDictApplicationManagerBase, IB public async Task EnsureBackOfficeApplicationAsync(Uri backOfficeUrl, CancellationToken cancellationToken = default) { - if (_runtimeState.Level < RuntimeLevel.Run) + // Install is okay without this, because we do not need a token to install, + // but upgrades do, so we need to execute for everything higher then or equal to upgrade. + if (_runtimeState.Level < RuntimeLevel.Upgrade) { return; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrentUserResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrentUserResponseModel.cs index d8fc5ab4f6..d3b5477a7c 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrentUserResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrentUserResponseModel.cs @@ -1,3 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.UserGroup; +using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; +using Umbraco.Cms.Core.Models.Membership; + namespace Umbraco.Cms.Api.Management.ViewModels.User.Current; public class CurrentUserResponseModel @@ -22,5 +26,7 @@ public class CurrentUserResponseModel public required bool HasAccessToAllLanguages { get; init; } - public required ISet Permissions { get; init; } + public required ISet FallbackPermissions { get; init; } + public required ISet Permissions { get; init; } + public required ISet AllowedSections { get; init; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/UserPermissionViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/UserPermissionViewModel.cs index fe645acc4b..97723352f7 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/UserPermissionViewModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/UserPermissionViewModel.cs @@ -4,5 +4,5 @@ public class UserPermissionViewModel { public Guid NodeKey { get; set; } - public IEnumerable Permissions { get; set; } = Enumerable.Empty(); + public ISet Permissions { get; set; } = new HashSet(); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/DocumentPermissionViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/DocumentPermissionViewModel.cs new file mode 100644 index 0000000000..c925a5e71e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/DocumentPermissionViewModel.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; + +public class DocumentPermissionPresentationModel : IPermissionPresentationModel +{ + public required ReferenceByIdModel Document { get; set; } + public required ISet Verbs { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/IPermissionPresentationModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/IPermissionPresentationModel.cs new file mode 100644 index 0000000000..5bbeeae9b3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/IPermissionPresentationModel.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Api.Management.OpenApi; + +namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; + +public interface IPermissionPresentationModel : IOpenApiDiscriminator +{ + ISet Verbs { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/UnknownTypePermissionViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/UnknownTypePermissionViewModel.cs new file mode 100644 index 0000000000..3c06d41cd3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/UnknownTypePermissionViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; + +public class UnknownTypePermissionPresentationModel : IPermissionPresentationModel +{ + public required ISet Verbs { get; set; } + + public required string Context { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs index 5156d943c2..2338d89f6a 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup; +using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; + +namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup; /// /// @@ -68,7 +70,8 @@ public class UserGroupBase public bool MediaRootAccess { get; init; } /// - /// Ad-hoc list of permissions provided, and maintained by the front-end. The server has no concept of what these mean. + /// List of permissions provided, and maintained by the front-end. The server has no concept all of them, but some can be used on the server. /// - public required ISet Permissions { get; init; } + public required ISet FallbackPermissions { get; init; } + public required ISet Permissions { get; init; } } diff --git a/src/Umbraco.Core/Actions/ActionAssignDomain.cs b/src/Umbraco.Core/Actions/ActionAssignDomain.cs index 7ef7b9ca83..c321a9292d 100644 --- a/src/Umbraco.Core/Actions/ActionAssignDomain.cs +++ b/src/Umbraco.Core/Actions/ActionAssignDomain.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionAssignDomain : IAction { /// - public const char ActionLetter = 'I'; + public const string ActionLetter = "Umb.Document.CultureAndHostnames"; /// public const string ActionAlias = "assigndomain"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionBrowse.cs b/src/Umbraco.Core/Actions/ActionBrowse.cs index ff217ac43a..d18e9ef531 100644 --- a/src/Umbraco.Core/Actions/ActionBrowse.cs +++ b/src/Umbraco.Core/Actions/ActionBrowse.cs @@ -17,13 +17,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionBrowse : IAction { /// - public const char ActionLetter = 'F'; + public const string ActionLetter = "Umb.Document.Read"; /// public const string ActionAlias = "browse"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionCollection.cs b/src/Umbraco.Core/Actions/ActionCollection.cs index b204075b88..c0682acfa2 100644 --- a/src/Umbraco.Core/Actions/ActionCollection.cs +++ b/src/Umbraco.Core/Actions/ActionCollection.cs @@ -29,16 +29,15 @@ public class ActionCollection : BuilderCollectionBase where T : IAction => this.OfType().FirstOrDefault(); /// - /// Gets the actions by the specified letters + /// Gets the actions by the specified verbs /// - public IEnumerable GetByLetters(IEnumerable letters) + public ISet GetByVerbs(ISet verbs) { IAction[] actions = this.ToArray(); // no worry: internally, it's already an array - return letters - .Where(x => x.Length == 1) - .Select(x => actions.FirstOrDefault(y => y.Letter == x[0])) + return verbs + .Select(x => actions.FirstOrDefault(y => y.Letter == x)) .WhereNotNull() - .ToList(); + .ToHashSet(); } /// @@ -48,8 +47,7 @@ public class ActionCollection : BuilderCollectionBase { IAction[] actions = this.ToArray(); // no worry: internally, it's already an array return entityPermission.AssignedPermissions - .Where(x => x.Length == 1) - .SelectMany(x => actions.Where(y => y.Letter == x[0])) + .SelectMany(x => actions.Where(y => y.Letter == x)) .WhereNotNull() .ToList(); } diff --git a/src/Umbraco.Core/Actions/ActionCopy.cs b/src/Umbraco.Core/Actions/ActionCopy.cs index f4afb3906c..bee8940fcb 100644 --- a/src/Umbraco.Core/Actions/ActionCopy.cs +++ b/src/Umbraco.Core/Actions/ActionCopy.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionCopy : IAction { /// - public const char ActionLetter = 'O'; + public const string ActionLetter = "Umb.Document.Duplicate"; /// public const string ActionAlias = "copy"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs b/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs index f23e5cac84..16a1f6af8d 100644 --- a/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs +++ b/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionCreateBlueprintFromContent : IAction { /// - public const char ActionLetter = 'ï'; + public const string ActionLetter = "Umb.Document.CreateBlueprint"; /// public const string ActionAlias = "createblueprint"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionDelete.cs b/src/Umbraco.Core/Actions/ActionDelete.cs index ea8517c794..70d3da72eb 100644 --- a/src/Umbraco.Core/Actions/ActionDelete.cs +++ b/src/Umbraco.Core/Actions/ActionDelete.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionDelete : IAction { /// - public const char ActionLetter = 'D'; + public const string ActionLetter = "Umb.Document.Delete"; /// public const string ActionAlias = "delete"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionMove.cs b/src/Umbraco.Core/Actions/ActionMove.cs index a145e86fab..9bb850447b 100644 --- a/src/Umbraco.Core/Actions/ActionMove.cs +++ b/src/Umbraco.Core/Actions/ActionMove.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionMove : IAction { /// - public const char ActionLetter = 'M'; + public const string ActionLetter = "Umb.Document.Move"; /// public const string ActionAlias = "move"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionNew.cs b/src/Umbraco.Core/Actions/ActionNew.cs index 25ac603532..fc3efeeba1 100644 --- a/src/Umbraco.Core/Actions/ActionNew.cs +++ b/src/Umbraco.Core/Actions/ActionNew.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionNew : IAction { /// - public const char ActionLetter = 'C'; + public const string ActionLetter = "Umb.Document.Create"; /// public const string ActionAlias = "create"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionNotify.cs b/src/Umbraco.Core/Actions/ActionNotify.cs index 8ad650a74a..4d21f66406 100644 --- a/src/Umbraco.Core/Actions/ActionNotify.cs +++ b/src/Umbraco.Core/Actions/ActionNotify.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionNotify : IAction { /// - public const char ActionLetter = 'N'; + public const string ActionLetter = "Umb.Document.Notifications"; /// public const string ActionAlias = "notify"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionProtect.cs b/src/Umbraco.Core/Actions/ActionProtect.cs index 21c985961b..8ba85bcdd8 100644 --- a/src/Umbraco.Core/Actions/ActionProtect.cs +++ b/src/Umbraco.Core/Actions/ActionProtect.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionProtect : IAction { /// - public const char ActionLetter = 'P'; + public const string ActionLetter = "Umb.Document.PublicAccess"; /// public const string ActionAlias = "protect"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionPublish.cs b/src/Umbraco.Core/Actions/ActionPublish.cs index 09f6c831e9..c3098a17e6 100644 --- a/src/Umbraco.Core/Actions/ActionPublish.cs +++ b/src/Umbraco.Core/Actions/ActionPublish.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionPublish : IAction { /// - public const char ActionLetter = 'U'; + public const string ActionLetter = "Umb.Document.Publish"; /// public const string ActionAlias = "publish"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionRestore.cs b/src/Umbraco.Core/Actions/ActionRestore.cs index dcdfc86521..646134aaac 100644 --- a/src/Umbraco.Core/Actions/ActionRestore.cs +++ b/src/Umbraco.Core/Actions/ActionRestore.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionRestore : IAction { /// - public const char ActionLetter = 'V'; + public const string ActionLetter = "Umb.DocumentRecycleBin.Restore"; /// public const string ActionAlias = "restore"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionRights.cs b/src/Umbraco.Core/Actions/ActionRights.cs index 7e493d6c16..af39ac2f9e 100644 --- a/src/Umbraco.Core/Actions/ActionRights.cs +++ b/src/Umbraco.Core/Actions/ActionRights.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionRights : IAction { /// - public const char ActionLetter = 'R'; + public const string ActionLetter = "Umb.Document.Permissions"; /// public const string ActionAlias = "rights"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionRollback.cs b/src/Umbraco.Core/Actions/ActionRollback.cs index 7e1dab6467..eea850cb35 100644 --- a/src/Umbraco.Core/Actions/ActionRollback.cs +++ b/src/Umbraco.Core/Actions/ActionRollback.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionRollback : IAction { /// - public const char ActionLetter = 'K'; + public const string ActionLetter = "Umb.Document.Rollback"; /// public const string ActionAlias = "rollback"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionSort.cs b/src/Umbraco.Core/Actions/ActionSort.cs index 4f90e404c8..0060fa729b 100644 --- a/src/Umbraco.Core/Actions/ActionSort.cs +++ b/src/Umbraco.Core/Actions/ActionSort.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionSort : IAction { /// - public const char ActionLetter = 'S'; + public const string ActionLetter = "Umb.Document.Sort"; /// public const string ActionAlias = "sort"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionToPublish.cs b/src/Umbraco.Core/Actions/ActionToPublish.cs index 25719e30fc..a980d819aa 100644 --- a/src/Umbraco.Core/Actions/ActionToPublish.cs +++ b/src/Umbraco.Core/Actions/ActionToPublish.cs @@ -10,13 +10,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionToPublish : IAction { /// - public const char ActionLetter = 'H'; + public const string ActionLetter = "Umb.Document.SendForApproval"; /// public const string ActionAlias = "sendtopublish"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionUnpublish.cs b/src/Umbraco.Core/Actions/ActionUnpublish.cs index f10159b403..30a5f27a2f 100644 --- a/src/Umbraco.Core/Actions/ActionUnpublish.cs +++ b/src/Umbraco.Core/Actions/ActionUnpublish.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionUnpublish : IAction { /// - public const char ActionLetter = 'Z'; + public const string ActionLetter = "Umb.Document.Unpublish"; /// public const string ActionAlias = "unpublish"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/ActionUpdate.cs b/src/Umbraco.Core/Actions/ActionUpdate.cs index aa6b0e9950..b694c378ba 100644 --- a/src/Umbraco.Core/Actions/ActionUpdate.cs +++ b/src/Umbraco.Core/Actions/ActionUpdate.cs @@ -9,13 +9,13 @@ namespace Umbraco.Cms.Core.Actions; public class ActionUpdate : IAction { /// - public const char ActionLetter = 'A'; + public const string ActionLetter = "Umb.Document.Update"; /// public const string ActionAlias = "update"; /// - public char Letter => ActionLetter; + public string Letter => ActionLetter; /// public string Alias => ActionAlias; diff --git a/src/Umbraco.Core/Actions/IAction.cs b/src/Umbraco.Core/Actions/IAction.cs index 6afe147e69..d63e912fa8 100644 --- a/src/Umbraco.Core/Actions/IAction.cs +++ b/src/Umbraco.Core/Actions/IAction.cs @@ -14,15 +14,15 @@ namespace Umbraco.Cms.Core.Actions; public interface IAction : IDiscoverable { /// - const char ActionLetter = default; + const string ActionLetter = ""; /// - const string ActionAlias = default; + const string ActionAlias = ""; /// /// Gets the letter used to assign a permission (must be unique). /// - char Letter { get; } + string Letter { get; } /// /// Gets a value indicating whether whether to allow subscribing to notifications for this action diff --git a/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs index 142be715f7..23393b77f0 100644 --- a/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs +++ b/src/Umbraco.Core/Handlers/AuditNotificationsHandler.cs @@ -123,7 +123,7 @@ public sealed class AuditNotificationsHandler : foreach (EntityPermission perm in perms) { IUserGroup? group = _userGroupService.GetAsync(perm.UserGroupId).Result; - var assigned = string.Join(", ", perm.AssignedPermissions ?? Array.Empty()); + var assigned = string.Join(", ", perm.AssignedPermissions); IEntitySlim? entity = _entityService.Get(perm.EntityId); _auditService.Write( @@ -238,10 +238,10 @@ public sealed class AuditNotificationsHandler : IUserGroup group = groupWithUser.UserGroup; var dp = string.Join(", ", ((UserGroup)group).GetWereDirtyProperties()); - var sections = ((UserGroup)group).WasPropertyDirty("AllowedSections") + var sections = ((UserGroup)group).WasPropertyDirty(nameof(group.AllowedSections)) ? string.Join(", ", group.AllowedSections) : null; - var perms = ((UserGroup)group).WasPropertyDirty("Permissions") && group.Permissions is not null + var perms = ((UserGroup)group).WasPropertyDirty(nameof(group.Permissions)) ? string.Join(", ", group.Permissions) : null; diff --git a/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs b/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs index d32f2fa9fe..72eadd3130 100644 --- a/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/UserGroupSave.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.ContentEditing; @@ -41,17 +42,14 @@ public class UserGroupSave : EntityBasic, IValidatableObject /// A set of ad-hoc permissions provided by the frontend. /// /// - /// By default the server has no concept of what these strings mean, we simple store them and return them to the UI. - /// FIXME: Permissions already exists in the form of "DefaultPermissions", but is subject to change in the future - /// when we know more about how we want to handle permissions, potentially those will be migrated in the these "soft" permissions. + /// By default the server has no concept of what all of these strings mean, we simple store them and return them to the UI. /// - public ISet? Permissions { get; set; } + public required ISet Permissions { get; set; } /// - /// The list of letters (permission codes) to assign as the default for the user group + /// A set of granular permissions /// - [DataMember(Name = "defaultPermissions")] - public IEnumerable? DefaultPermissions { get; set; } + public required ISet GranularPermissions { get; set; } /// /// The assigned permissions for content @@ -76,7 +74,7 @@ public class UserGroupSave : EntityBasic, IValidatableObject public IEnumerable Validate(ValidationContext validationContext) { - if (DefaultPermissions?.Any(x => x.IsNullOrWhiteSpace()) ?? false) + if (Permissions.Any(x => x.IsNullOrWhiteSpace()) || GranularPermissions.Any(x=>x.Permission.IsNullOrWhiteSpace())) { yield return new ValidationResult("A permission value cannot be null or empty", new[] { "Permissions" }); } diff --git a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs index edd25966e1..fe6df58824 100644 --- a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs @@ -130,10 +130,10 @@ public class UserMapDefinition : IMapDefinition target.Icon = source.Icon; target.Alias = source.Alias; target.Name = source.Name; - target.Permissions = source.DefaultPermissions; + target.Permissions = source.Permissions; + target.GranularPermissions = source.GranularPermissions; target.Key = source.Key; target.HasAccessToAllLanguages = source.HasAccessToAllLanguages; - target.PermissionNames = source.Permissions ?? new HashSet(); var id = GetIntId(source.Id); if (id > 0) @@ -510,7 +510,7 @@ public class UserMapDefinition : IMapDefinition Description = _textService.Localize("actionDescriptions", action.Alias), Icon = action.Icon, Checked = source.Permissions != null && - source.Permissions.Contains(action.Letter.ToString(CultureInfo.InvariantCulture)), + source.Permissions.Contains(action.Letter), PermissionCode = action.Letter.ToString(CultureInfo.InvariantCulture), }; } diff --git a/src/Umbraco.Core/Models/Membership/EntityPermission.cs b/src/Umbraco.Core/Models/Membership/EntityPermission.cs index 58e84f27f9..e8ec1f5ab4 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermission.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermission.cs @@ -5,7 +5,7 @@ namespace Umbraco.Cms.Core.Models.Membership; /// public class EntityPermission : IEquatable { - public EntityPermission(int groupId, int entityId, string[] assignedPermissions) + public EntityPermission(int groupId, int entityId, ISet assignedPermissions) { UserGroupId = groupId; EntityId = entityId; @@ -13,7 +13,7 @@ public class EntityPermission : IEquatable IsDefaultPermissions = false; } - public EntityPermission(int groupId, int entityId, string[] assignedPermissions, bool isDefaultPermissions) + public EntityPermission(int groupId, int entityId, ISet assignedPermissions, bool isDefaultPermissions) { UserGroupId = groupId; EntityId = entityId; @@ -28,7 +28,7 @@ public class EntityPermission : IEquatable /// /// The assigned permissions for the user/entity combo /// - public string[] AssignedPermissions { get; } + public ISet AssignedPermissions { get; } /// /// True if the permissions assigned to this object are the group's default permissions and not explicitly defined diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs index 727f7964f7..5445a01965 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs @@ -5,9 +5,9 @@ namespace Umbraco.Cms.Core.Models.Membership; /// public class EntityPermissionCollection : HashSet { - private Dictionary? _aggregateNodePermissions; + private Dictionary>? _aggregateNodePermissions; - private string[]? _aggregatePermissions; + private ISet? _aggregatePermissions; public EntityPermissionCollection() { @@ -25,17 +25,17 @@ public class EntityPermissionCollection : HashSet /// /// This value is only calculated once per node /// - public IEnumerable GetAllPermissions(int entityId) + public ISet GetAllPermissions(int entityId) { if (_aggregateNodePermissions == null) { - _aggregateNodePermissions = new Dictionary(); + _aggregateNodePermissions = new Dictionary>(); } - if (_aggregateNodePermissions.TryGetValue(entityId, out string[]? entityPermissions) == false) + if (_aggregateNodePermissions.TryGetValue(entityId, out ISet? entityPermissions) == false) { entityPermissions = this.Where(x => x.EntityId == entityId).SelectMany(x => x.AssignedPermissions) - .Distinct().ToArray(); + .ToHashSet(); _aggregateNodePermissions[entityId] = entityPermissions; } @@ -49,7 +49,7 @@ public class EntityPermissionCollection : HashSet /// /// This value is only calculated once /// - public IEnumerable GetAllPermissions() => + public ISet GetAllPermissions() => _aggregatePermissions ??= - this.SelectMany(x => x.AssignedPermissions).Distinct().ToArray(); + this.SelectMany(x => x.AssignedPermissions).Distinct().ToHashSet(); } diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs index fce893c710..26385052cc 100644 --- a/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs +++ b/src/Umbraco.Core/Models/Membership/EntityPermissionSet.cs @@ -37,5 +37,5 @@ public class EntityPermissionSet /// /// This value is only calculated once /// - public IEnumerable GetAllPermissions() => PermissionsSet.GetAllPermissions(); + public ISet GetAllPermissions() => PermissionsSet.GetAllPermissions(); } diff --git a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs index e7095c9c8c..fa2110cbcf 100644 --- a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs @@ -1,3 +1,5 @@ +using Umbraco.Cms.Core.Models.Membership.Permissions; + namespace Umbraco.Cms.Core.Models.Membership; /// @@ -25,15 +27,10 @@ public interface IReadOnlyUserGroup // This is set to return true as default to avoid breaking changes. bool HasAccessToAllLanguages => true; - /// - /// The set of default permissions - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more - /// flexible permissions structure in the future. - /// - IEnumerable? Permissions { get; set; } - ISet PermissionNames { get; } + ISet Permissions { get; } + + ISet GranularPermissions { get; } + IEnumerable AllowedSections { get; } IEnumerable AllowedLanguages => Enumerable.Empty(); diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index c78ee7cbd5..c4adc5999b 100644 --- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -1,5 +1,6 @@ using System.Collections; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership.Permissions; namespace Umbraco.Cms.Core.Models.Membership; @@ -31,24 +32,15 @@ public interface IUserGroup : IEntity, IRememberBeingDirty set { /* This is NoOp to avoid breaking changes */ } } - /// - /// The set of default permissions - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more - /// flexible permissions structure in the future. - /// - IEnumerable? Permissions { get; set; } - /// /// The set of permissions provided by the frontend. /// /// - /// By default the server has no concept of what these strings mean, we simple store them and return them to the UI. - /// FIXME: For now this is named PermissionNames since Permissions already exists, but is subject to change in the future - /// when we know more about how we want to handle permissions, potentially those will be migrated in the these "soft" permissions. + /// By default the server has no concept of what all of these strings mean, we simple store them and return them to the UI. /// - ISet PermissionNames { get; set; } + ISet Permissions { get; set; } + + ISet GranularPermissions { get; set; } IEnumerable AllowedSections { get; } diff --git a/src/Umbraco.Core/Models/Membership/Permissions/DocumentGranularPermission.cs b/src/Umbraco.Core/Models/Membership/Permissions/DocumentGranularPermission.cs new file mode 100644 index 0000000000..488a98529f --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/Permissions/DocumentGranularPermission.cs @@ -0,0 +1,37 @@ +namespace Umbraco.Cms.Core.Models.Membership.Permissions; + + +public class DocumentGranularPermission : INodeGranularPermission +{ + public const string ContextType = "Document"; + + public required Guid Key { get; set; } + + public string Context => ContextType; + + public required string Permission { get; set; } + + protected bool Equals(DocumentGranularPermission other) => Key.Equals(other.Key) && Permission == other.Permission; + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((DocumentGranularPermission)obj); + } + + public override int GetHashCode() => HashCode.Combine(Key, Permission); +} diff --git a/src/Umbraco.Core/Models/Membership/Permissions/IGranularPermission.cs b/src/Umbraco.Core/Models/Membership/Permissions/IGranularPermission.cs new file mode 100644 index 0000000000..752e912969 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/Permissions/IGranularPermission.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.Models.Membership.Permissions; + +public interface IGranularPermission +{ + public string Context { get; } + + public Guid? Key => null; + + public string Permission { get; set; } +} diff --git a/src/Umbraco.Core/Models/Membership/Permissions/INodeGranularPermission.cs b/src/Umbraco.Core/Models/Membership/Permissions/INodeGranularPermission.cs new file mode 100644 index 0000000000..3c0a050424 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/Permissions/INodeGranularPermission.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Core.Models.Membership.Permissions; + +public interface INodeGranularPermission : IGranularPermission +{ + new Guid Key { get; set; } + + Guid? IGranularPermission.Key + { + get => Key; + } +} diff --git a/src/Umbraco.Core/Models/Membership/Permissions/UnknownTypeGranularPermission.cs b/src/Umbraco.Core/Models/Membership/Permissions/UnknownTypeGranularPermission.cs new file mode 100644 index 0000000000..caf75f2796 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/Permissions/UnknownTypeGranularPermission.cs @@ -0,0 +1,32 @@ +namespace Umbraco.Cms.Core.Models.Membership.Permissions; + +public class UnknownTypeGranularPermission : IGranularPermission +{ + public required string Context { get; set; } + + public required string Permission { get; set; } + + protected bool Equals(UnknownTypeGranularPermission other) => Context == other.Context && Permission == other.Permission; + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((UnknownTypeGranularPermission)obj); + } + + public override int GetHashCode() => HashCode.Combine(Context, Permission); +} diff --git a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs index 9966b83d10..91bd6daca5 100644 --- a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs @@ -1,3 +1,5 @@ +using Umbraco.Cms.Core.Models.Membership.Permissions; + namespace Umbraco.Cms.Core.Models.Membership; public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable @@ -12,8 +14,8 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable allowedLanguages, IEnumerable allowedSections, - IEnumerable? permissions, - ISet permissionNames, + ISet permissions, + ISet granularPermissions, bool hasAccessToAllLanguages) { Name = name ?? string.Empty; @@ -23,13 +25,13 @@ public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable - /// The set of default permissions - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more - /// flexible permissions structure in the future. - /// - public IEnumerable? Permissions { get; set; } - public IEnumerable AllowedLanguages { get; private set; } - public ISet PermissionNames { get; private set; } + + public ISet Permissions { get; private set; } + + public ISet GranularPermissions { get; private set; } + public IEnumerable AllowedSections { get; private set; } public static bool operator ==(ReadOnlyUserGroup left, ReadOnlyUserGroup right) => Equals(left, right); diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index 119bc6bf69..c512702d54 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Runtime.Serialization; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; @@ -19,13 +20,19 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), enum1 => enum1.GetHashCode()); + private static readonly DelegateEqualityComparer> _granularPermissionSetComparer = + new( + (set1, set2) => Equals(set1, set2), + set => set.GetHashCode()); + + private readonly IShortStringHelper _shortStringHelper; private string _alias; private string? _icon; private string _name; private bool _hasAccessToAllLanguages; - private IEnumerable? _permissions; - private ISet _permissionNames = new HashSet(); + private ISet _permissions; + private ISet _granularPermissions; private List _sectionCollection; private List _languageCollection; private int? _startContentId; @@ -41,6 +48,8 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup _shortStringHelper = shortStringHelper; _sectionCollection = new List(); _languageCollection = new List(); + _permissions = new HashSet(); + _granularPermissions = new HashSet(); } /// @@ -50,6 +59,7 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup /// /// /// + /// /// /// public UserGroup( @@ -57,14 +67,12 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup int userCount, string? alias, string? name, - IEnumerable permissions, string? icon) : this(shortStringHelper) { UserCount = userCount; _alias = alias ?? string.Empty; _name = name ?? string.Empty; - _permissions = permissions; _icon = icon; } @@ -112,27 +120,20 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup set => SetPropertyValueAndDetectChanges(value, ref _hasAccessToAllLanguages, nameof(HasAccessToAllLanguages)); } - /// - /// The set of default permissions for the user group - /// - /// - /// By default each permission is simply a single char but we've made this an enumerable{string} to support a more - /// flexible permissions structure in the future. - /// - [DataMember] - public IEnumerable? Permissions + /// + public ISet Permissions { get => _permissions; - set => SetPropertyValueAndDetectChanges(value, ref _permissions, nameof(Permissions), _stringEnumerableComparer); + set => SetPropertyValueAndDetectChanges(value, ref _permissions!, nameof(Permissions), _stringEnumerableComparer); } - /// - public ISet PermissionNames + public ISet GranularPermissions { - get => _permissionNames; - set => SetPropertyValueAndDetectChanges(value, ref _permissionNames!, nameof(PermissionNames), _stringEnumerableComparer); + get => _granularPermissions; + set => SetPropertyValueAndDetectChanges(value, ref _granularPermissions!, nameof(GranularPermissions), _granularPermissionSetComparer); } + public IEnumerable AllowedSections => _sectionCollection; public int UserCount { get; } diff --git a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs index 9b078cc814..8c5940ac75 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs @@ -25,7 +25,7 @@ public static class UserGroupExtensions group.AllowedLanguages, group.AllowedSections, group.Permissions, - group.PermissionNames, + group.GranularPermissions, group.HasAccessToAllLanguages); } diff --git a/src/Umbraco.Core/Models/NodePermissions.cs b/src/Umbraco.Core/Models/NodePermissions.cs index 9e89d925e7..b5a48b3e9f 100644 --- a/src/Umbraco.Core/Models/NodePermissions.cs +++ b/src/Umbraco.Core/Models/NodePermissions.cs @@ -7,5 +7,5 @@ public class NodePermissions { public Guid NodeKey { get; set; } - public IEnumerable Permissions { get; set; } = Array.Empty(); + public ISet Permissions { get; set; } = new HashSet(); } diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index fead185fd6..07d7fb520c 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -53,9 +53,14 @@ public static partial class Constants public const string User2UserGroup = TableNamePrefix + "User2UserGroup"; public const string User2NodeNotify = TableNamePrefix + "User2NodeNotify"; public const string UserGroup2App = TableNamePrefix + "UserGroup2App"; + + [Obsolete("Will be removed in Umbraco 18 as this table haven't existed since Umbraco 14.")] public const string UserGroup2Node = TableNamePrefix + "UserGroup2Node"; - public const string UserGroup2Permission = TableNamePrefix + "UserGroup2Permission"; + [Obsolete("Will be removed in Umbraco 18 as this table haven't existed since Umbraco 14.")] public const string UserGroup2NodePermission = TableNamePrefix + "UserGroup2NodePermission"; + + public const string UserGroup2Permission = TableNamePrefix + "UserGroup2Permission"; + public const string UserGroup2GranularPermission = TableNamePrefix + "UserGroup2GranularPermission"; public const string UserGroup2Language = TableNamePrefix + "UserGroup2Language"; public const string ExternalLogin = TableNamePrefix + "ExternalLogin"; public const string TwoFactorLogin = TableNamePrefix + "TwoFactorLogin"; diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index b51f02fd54..f6ee0b6926 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -36,7 +36,7 @@ public interface IDocumentRepository : IContentRepository, IReadR /// Gets objects having an expiration date before (lower than, or equal to) a specified date. /// /// - /// The content returned from this method may be culture variant, in which case you can use + /// The content returned from this method may be culture variant, in which case you can use /// to get the status for a specific culture. /// IEnumerable GetContentForExpiration(DateTime date); @@ -45,7 +45,7 @@ public interface IDocumentRepository : IContentRepository, IReadR /// Gets objects having a release date before (lower than, or equal to) a specified date. /// /// - /// The content returned from this method may be culture variant, in which case you can use + /// The content returned from this method may be culture variant, in which case you can use /// to get the status for a specific culture. /// IEnumerable GetContentForRelease(DateTime date); @@ -74,7 +74,7 @@ public interface IDocumentRepository : IContentRepository, IReadR /// /// /// - void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds); + void AssignEntityPermission(IContent entity, string permission, IEnumerable groupIds); /// /// Gets the explicit list of permissions for the content item diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index 2914a80aca..d225651eef 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -48,6 +48,8 @@ public interface IEntityRepository : IRepository bool Exists(Guid key); + bool Exists(IEnumerable keys); + /// /// Asserts if an entity with the given object type exists. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs index 0959019af2..4a9b760ca7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUserGroupRepository.cs @@ -51,7 +51,7 @@ public interface IUserGroupRepository : IReadWriteQueryRepository - void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds); + void ReplaceGroupPermissions(int groupId, ISet permissions, params int[] entityIds); /// /// Assigns the same permission set for a single group to any number of entities @@ -59,5 +59,5 @@ public interface IUserGroupRepository : IReadWriteQueryRepositoryId of group /// Permissions as enumerable list of /// Specify the nodes to replace permissions for - void AssignGroupPermission(int groupId, char permission, params int[] entityIds); + void AssignGroupPermission(int groupId, string permission, params int[] entityIds); } diff --git a/src/Umbraco.Core/Security/ContentPermissions.cs b/src/Umbraco.Core/Security/ContentPermissions.cs index bdd6b5179d..31b4556934 100644 --- a/src/Umbraco.Core/Security/ContentPermissions.cs +++ b/src/Umbraco.Core/Security/ContentPermissions.cs @@ -74,13 +74,13 @@ public class ContentPermissions public ContentAccess CheckPermissions( IContent content, IUser user, - char permissionToCheck) => CheckPermissions(content, user, new[] { permissionToCheck }); + string permissionToCheck) => CheckPermissions(content, user, new HashSet(){ permissionToCheck }); [Obsolete($"Please use {nameof(IContentPermissionService)} instead, scheduled for removal in V15.")] public ContentAccess CheckPermissions( IContent? content, IUser? user, - IReadOnlyList permissionsToCheck) + IReadOnlySet permissionsToCheck) { if (user == null) { @@ -114,13 +114,13 @@ public class ContentPermissions public ContentAccess CheckPermissions( IUmbracoEntity entity, IUser? user, - char permissionToCheck) => CheckPermissions(entity, user, new[] { permissionToCheck }); + string permissionToCheck) => CheckPermissions(entity, user, new HashSet(){ permissionToCheck }); [Obsolete($"Please use {nameof(IContentPermissionService)} instead, scheduled for removal in V15.")] public ContentAccess CheckPermissions( IUmbracoEntity entity, IUser? user, - IReadOnlyList permissionsToCheck) + IReadOnlySet permissionsToCheck) { if (user == null) { @@ -163,7 +163,7 @@ public class ContentPermissions int nodeId, IUser user, out IUmbracoEntity? entity, - IReadOnlyList? permissionsToCheck = null) + IReadOnlySet? permissionsToCheck = null) { if (user == null) { @@ -223,7 +223,7 @@ public class ContentPermissions int nodeId, IUser? user, out IContent? contentItem, - IReadOnlyList? permissionsToCheck = null) + IReadOnlySet? permissionsToCheck = null) { if (user == null) { @@ -271,11 +271,11 @@ public class ContentPermissions } [Obsolete($"Please use {nameof(IContentPermissionService)} instead, scheduled for removal in V15.")] - private bool CheckPermissionsPath(string? path, IUser user, IReadOnlyList? permissionsToCheck = null) + private bool CheckPermissionsPath(string? path, IUser user, IReadOnlySet? permissionsToCheck = null) { if (permissionsToCheck == null) { - permissionsToCheck = Array.Empty(); + permissionsToCheck = new HashSet(); } // get the implicit/inherited permissions for the user for this path diff --git a/src/Umbraco.Core/Services/ContentPermissionService.cs b/src/Umbraco.Core/Services/ContentPermissionService.cs index 2e24bddd1f..e8b2d45fcb 100644 --- a/src/Umbraco.Core/Services/ContentPermissionService.cs +++ b/src/Umbraco.Core/Services/ContentPermissionService.cs @@ -35,7 +35,7 @@ internal sealed class ContentPermissionService : IContentPermissionService public async Task AuthorizeAccessAsync( IUser user, IEnumerable contentKeys, - ISet permissionsToCheck) + ISet permissionsToCheck) { var contentItems = _contentService.GetByIds(contentKeys).ToArray(); @@ -58,7 +58,7 @@ internal sealed class ContentPermissionService : IContentPermissionService public async Task AuthorizeDescendantsAccessAsync( IUser user, Guid parentKey, - ISet permissionsToCheck) + ISet permissionsToCheck) { var denied = new List(); var page = 0; @@ -103,7 +103,7 @@ internal sealed class ContentPermissionService : IContentPermissionService } /// - public async Task AuthorizeRootAccessAsync(IUser user, ISet permissionsToCheck) + public async Task AuthorizeRootAccessAsync(IUser user, ISet permissionsToCheck) { var hasAccess = user.HasContentRootAccess(_entityService, _appCaches); @@ -119,7 +119,7 @@ internal sealed class ContentPermissionService : IContentPermissionService } /// - public async Task AuthorizeBinAccessAsync(IUser user, ISet permissionsToCheck) + public async Task AuthorizeBinAccessAsync(IUser user, ISet permissionsToCheck) { var hasAccess = user.HasContentBinAccess(_entityService, _appCaches); @@ -157,7 +157,7 @@ internal sealed class ContentPermissionService : IContentPermissionService /// The paths of the content items to check for access. /// The permissions to authorize. /// true if the user has the required permissions; otherwise, false. - private bool HasPermissionAccess(IUser user, IEnumerable contentPaths, IEnumerable permissionsToCheck) + private bool HasPermissionAccess(IUser user, IEnumerable contentPaths, ISet permissionsToCheck) { foreach (var path in contentPaths) { @@ -166,7 +166,7 @@ internal sealed class ContentPermissionService : IContentPermissionService foreach (var p in permissionsToCheck) { - if (permissionSet.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + if (permissionSet.GetAllPermissions().Contains(p) == false) { return false; } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index b0b8d164e8..291655e935 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -231,7 +231,7 @@ public class ContentService : RepositoryService, IContentService /// /// /// - public void SetPermission(IContent entity, char permission, IEnumerable groupIds) + public void SetPermission(IContent entity, string permission, IEnumerable groupIds) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index e52a4712de..bf7cf3d6fa 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -124,6 +124,14 @@ public class EntityService : RepositoryService, IEntityService } } + public bool Exists(IEnumerable keys) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _entityRepository.Exists(keys); + } + } + /// public bool Exists(Guid key, UmbracoObjectTypes objectType) { diff --git a/src/Umbraco.Core/Services/IContentPermissionService.cs b/src/Umbraco.Core/Services/IContentPermissionService.cs index bb1d2e5bff..b6ee049ec7 100644 --- a/src/Umbraco.Core/Services/IContentPermissionService.cs +++ b/src/Umbraco.Core/Services/IContentPermissionService.cs @@ -16,8 +16,8 @@ public interface IContentPermissionService /// The identifier of the content item to check for access. /// The permission to authorize. /// A task resolving into a . - Task AuthorizeAccessAsync(IUser user, Guid contentKey, char permissionToCheck) - => AuthorizeAccessAsync(user, contentKey.Yield(), new HashSet { permissionToCheck }); + Task AuthorizeAccessAsync(IUser user, Guid contentKey, string permissionToCheck) + => AuthorizeAccessAsync(user, contentKey.Yield(), new HashSet { permissionToCheck }); /// /// Authorize that a user has access to content items. @@ -26,7 +26,7 @@ public interface IContentPermissionService /// The identifiers of the content items to check for access. /// The collection of permissions to authorize. /// A task resolving into a . - Task AuthorizeAccessAsync(IUser user, IEnumerable contentKeys, ISet permissionsToCheck); + Task AuthorizeAccessAsync(IUser user, IEnumerable contentKeys, ISet permissionsToCheck); /// /// Authorize that a user has access to the descendant items of a content item. @@ -35,8 +35,8 @@ public interface IContentPermissionService /// The identifier of the parent content item to check its descendants for access. /// The permission to authorize. /// A task resolving into a . - Task AuthorizeDescendantsAccessAsync(IUser user, Guid parentKey, char permissionToCheck) - => AuthorizeDescendantsAccessAsync(user, parentKey, new HashSet { permissionToCheck }); + Task AuthorizeDescendantsAccessAsync(IUser user, Guid parentKey, string permissionToCheck) + => AuthorizeDescendantsAccessAsync(user, parentKey, new HashSet { permissionToCheck }); /// /// Authorize that a user has access to the descendant items of a content item. @@ -45,7 +45,7 @@ public interface IContentPermissionService /// The identifier of the parent content item to check its descendants for access. /// The collection of permissions to authorize. /// A task resolving into a . - Task AuthorizeDescendantsAccessAsync(IUser user, Guid parentKey, ISet permissionsToCheck); + Task AuthorizeDescendantsAccessAsync(IUser user, Guid parentKey, ISet permissionsToCheck); /// /// Authorize that a user is allowed to perform action on the content root item. @@ -53,8 +53,8 @@ public interface IContentPermissionService /// to authorize. /// The permission to authorize. /// A task resolving into a . - Task AuthorizeRootAccessAsync(IUser user, char permissionToCheck) - => AuthorizeRootAccessAsync(user, new HashSet { permissionToCheck }); + Task AuthorizeRootAccessAsync(IUser user, string permissionToCheck) + => AuthorizeRootAccessAsync(user, new HashSet { permissionToCheck }); /// /// Authorize that a user is allowed to perform actions on the content root item. @@ -62,7 +62,7 @@ public interface IContentPermissionService /// to authorize. /// The collection of permissions to authorize. /// A task resolving into a . - Task AuthorizeRootAccessAsync(IUser user, ISet permissionsToCheck); + Task AuthorizeRootAccessAsync(IUser user, ISet permissionsToCheck); /// /// Authorize that a user is allowed to perform action on the content bin item. @@ -70,8 +70,8 @@ public interface IContentPermissionService /// to authorize. /// The permission to authorize. /// A task resolving into a . - Task AuthorizeBinAccessAsync(IUser user, char permissionToCheck) - => AuthorizeBinAccessAsync(user, new HashSet { permissionToCheck }); + Task AuthorizeBinAccessAsync(IUser user, string permissionToCheck) + => AuthorizeBinAccessAsync(user, new HashSet { permissionToCheck }); /// /// Authorize that a user is allowed to perform actions on the content bin item. @@ -79,7 +79,7 @@ public interface IContentPermissionService /// to authorize. /// The collection of permissions to authorize. /// A task resolving into a . - Task AuthorizeBinAccessAsync(IUser user, ISet permissionsToCheck); + Task AuthorizeBinAccessAsync(IUser user, ISet permissionsToCheck); /// /// Authorize that a user has access to specific cultures diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 577e5789f7..1f424c0892 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -484,7 +484,7 @@ public interface IContentService : IContentServiceBase /// Assigns a permission to a document. /// /// Adds the permission to existing permissions. - void SetPermission(IContent entity, char permission, IEnumerable groupIds); + void SetPermission(IContent entity, string permission, IEnumerable groupIds); #endregion diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 02959841dd..40a7567cdd 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -60,6 +60,12 @@ public interface IEntityService /// The unique key of the entity. bool Exists(Guid key); + /// + /// Determines whether an entity exists. + /// + /// The unique keys of the entities. + bool Exists(IEnumerable keys); + /// /// Determines whether and entity of a certain object type exists. /// diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 8589346dec..b95a53d99b 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -294,7 +294,7 @@ public interface IUserService : IMembershipUserService /// removed. /// /// If no 'entityIds' are specified all permissions will be removed for the specified group. - void ReplaceUserGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds); + void ReplaceUserGroupPermissions(int groupId, ISet permissions, params int[] entityIds); /// /// Assigns the same permission set for a single user group to any number of entities @@ -302,7 +302,7 @@ public interface IUserService : IMembershipUserService /// Id of the group /// /// Specify the nodes to replace permissions for - void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds); + void AssignUserGroupPermission(int groupId, string permission, params int[] entityIds); /// /// Gets a list of objects associated with a given group diff --git a/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs index 9165db99e4..0cbfcf2292 100644 --- a/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs @@ -13,6 +13,7 @@ public enum UserGroupOperationStatus CancelledByNotification, MediaStartNodeKeyNotFound, DocumentStartNodeKeyNotFound, + DocumentPermissionKeyNotFound, LanguageNotFound, NameTooLong, AliasTooLong, diff --git a/src/Umbraco.Core/Services/UserGroupService.cs b/src/Umbraco.Core/Services/UserGroupService.cs index e0c401a888..03431f755e 100644 --- a/src/Umbraco.Core/Services/UserGroupService.cs +++ b/src/Umbraco.Core/Services/UserGroupService.cs @@ -2,6 +2,7 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; @@ -456,6 +457,12 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService return startNodesValidationStatus; } + UserGroupOperationStatus granularPermissionsValidationStatus = ValidateGranularPermissionsExists(userGroup); + if (granularPermissionsValidationStatus is not UserGroupOperationStatus.Success) + { + return granularPermissionsValidationStatus; + } + return UserGroupOperationStatus.Success; } @@ -488,6 +495,25 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService return UserGroupOperationStatus.Success; } + private UserGroupOperationStatus ValidateGranularPermissionsExists(IUserGroup userGroup) + { + IEnumerable documentKeys = userGroup.GranularPermissions.Select(granularPermission => + { + if (granularPermission is DocumentGranularPermission nodeGranularPermission) + { + return (Guid?)nodeGranularPermission.Key; + } + + return null; + }).Where(x => x.HasValue).Cast().ToArray(); + if (documentKeys.Any() && _entityService.Exists(documentKeys) is false) + { + return UserGroupOperationStatus.DocumentPermissionKeyNotFound; + } + + return UserGroupOperationStatus.Success; + } + /// /// Ensures that the user creating the user group is either an admin, or in the group itself. /// diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index f3bccece06..027f34d0fc 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -105,7 +105,7 @@ internal class UserService : RepositoryService, IUserService if (permissions.Any(x => x.EntityId == nodeId)) { EntityPermission found = permissions.First(x => x.EntityId == nodeId); - var assignedPermissionsArray = found.AssignedPermissions.ToList(); + var assignedPermissionsArray = found.AssignedPermissions; // Working with permissions assigned directly to a user AND to their groups, so maybe several per node // and we need to get the most permissive set @@ -1801,7 +1801,7 @@ internal class UserService : RepositoryService, IUserService /// are removed. /// /// Specify the nodes to replace permissions for. - public void ReplaceUserGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + public void ReplaceUserGroupPermissions(int groupId, ISet permissions, params int[] entityIds) { if (entityIds.Length == 0) { @@ -1815,11 +1815,10 @@ internal class UserService : RepositoryService, IUserService _userGroupRepository.ReplaceGroupPermissions(groupId, permissions, entityIds); scope.Complete(); - var assigned = permissions?.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray(); - if (assigned is not null) + if (permissions is not null) { EntityPermission[] entityPermissions = - entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); + entityIds.Select(x => new EntityPermission(groupId, x, permissions)).ToArray(); scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); } } @@ -1831,7 +1830,7 @@ internal class UserService : RepositoryService, IUserService /// Id of the user group /// /// Specify the nodes to replace permissions for - public void AssignUserGroupPermission(int groupId, char permission, params int[] entityIds) + public void AssignUserGroupPermission(int groupId, string permission, params int[] entityIds) { if (entityIds.Length == 0) { @@ -1845,7 +1844,7 @@ internal class UserService : RepositoryService, IUserService _userGroupRepository.AssignGroupPermission(groupId, permission, entityIds); scope.Complete(); - var assigned = new[] { permission.ToString(CultureInfo.InvariantCulture) }; + var assigned = new HashSet() { permission }; EntityPermission[] entityPermissions = entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray(); scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs)); @@ -2179,7 +2178,7 @@ internal class UserService : RepositoryService, IUserService var results = new List(); foreach (KeyValuePair node in nodes) { - var permissions = permissionsCollection.GetAllPermissions(node.Value).ToArray(); + var permissions = permissionsCollection.GetAllPermissions(node.Value); results.Add(new NodePermissions { NodeKey = node.Key, Permissions = permissions }); } @@ -2231,7 +2230,7 @@ internal class UserService : RepositoryService, IUserService var results = new List(); foreach (int nodeId in idKeyMap.Keys) { - var permissions = permissionCollection.GetAllPermissions(nodeId).ToArray(); + var permissions = permissionCollection.GetAllPermissions(nodeId); results.Add(new NodePermissions { NodeKey = idKeyMap[nodeId], Permissions = permissions }); } @@ -2483,11 +2482,12 @@ internal class UserService : RepositoryService, IUserService return permissionsByEntityId[pathIds[0]]; } - private static void AddAdditionalPermissions(List assignedPermissions, string[] additionalPermissions) + private static void AddAdditionalPermissions(ISet assignedPermissions, ISet additionalPermissions) { - IEnumerable permissionsToAdd = additionalPermissions - .Where(x => assignedPermissions.Contains(x) == false); - assignedPermissions.AddRange(permissionsToAdd); + foreach (var additionalPermission in additionalPermissions) + { + assignedPermissions.Add(additionalPermission); + } } #endregion diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index ecce4f167f..e599a65792 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Globalization; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Membership; @@ -65,7 +66,7 @@ public static class UserServiceExtensions /// /// public static void RemoveUserGroupPermissions(this IUserService userService, int groupId, params int[] entityIds) => - userService.ReplaceUserGroupPermissions(groupId, null, entityIds); + userService.ReplaceUserGroupPermissions(groupId, new HashSet(), entityIds); /// /// Remove all permissions for this user group for all nodes @@ -73,7 +74,7 @@ public static class UserServiceExtensions /// /// public static void RemoveUserGroupPermissions(this IUserService userService, int groupId) => - userService.ReplaceUserGroupPermissions(groupId, null); + userService.ReplaceUserGroupPermissions(groupId, new HashSet()); public static IEnumerable GetProfilesById(this IUserService userService, params int[] ids) { diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 88fccefc66..6cea5550d2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; @@ -132,6 +133,11 @@ internal class DatabaseDataCreator CreateUserGroupData(); } + if (tableName.Equals(Constants.DatabaseSchema.Tables.UserGroup2Permission)) + { + CreateUserGroup2PermissionData(); + } + if (tableName.Equals(Constants.DatabaseSchema.Tables.User2UserGroup)) { CreateUser2UserGroupData(); @@ -185,6 +191,35 @@ internal class DatabaseDataCreator _logger.LogInformation("Completed creating data in {TableName}", tableName); } + private void CreateUserGroup2PermissionData() + { + var userGroupKeyToPermissions = new Dictionary>() + { + [Constants.Security.AdminGroupKey] = new []{ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionAssignDomain.ActionLetter, ActionPublish.ActionLetter, ActionRights.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "7", "T"}, + [Constants.Security.WriterGroupKey] = new []{ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionToPublish.ActionLetter, ActionBrowse.ActionLetter, ActionNotify.ActionLetter, ":"}, + [Constants.Security.EditorGroupKey] = new []{ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionPublish.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "T"}, + [Constants.Security.TranslatorGroupKey] = new []{ActionUpdate.ActionLetter, ActionBrowse.ActionLetter}, + }; + + var i = 1; + foreach (var (userGroupKey, permissions) in userGroupKeyToPermissions) + { + foreach (var permission in permissions) + { + _database.Insert( + Constants.DatabaseSchema.Tables.UserGroup2Permission, + "id", + false, + new UserGroup2PermissionDto + { + Id = i++, + UserGroupKey = userGroupKey, + Permission = permission, + }); + } + } + } + internal static Guid CreateUniqueRelationTypeId(string alias, string name) => (alias + "____" + name).ToGuid(); private void CreateNodeData() @@ -1222,7 +1257,6 @@ internal class DatabaseDataCreator StartContentId = -1, Alias = Constants.Security.AdminGroupAlias, Name = "Administrators", - DefaultPermissions = "CADMOSKTPIURZ:5F7ïN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-medal", @@ -1240,7 +1274,6 @@ internal class DatabaseDataCreator StartContentId = -1, Alias = Constants.Security.WriterGroupAlias, Name = "Writers", - DefaultPermissions = "CAH:FN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-edit", @@ -1258,7 +1291,6 @@ internal class DatabaseDataCreator StartContentId = -1, Alias = Constants.Security.EditorGroupAlias, Name = "Editors", - DefaultPermissions = "CADMOSKTPUZ:5FïN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-tools", @@ -1276,7 +1308,6 @@ internal class DatabaseDataCreator StartContentId = -1, Alias = Constants.Security.TranslatorGroupAlias, Name = "Translators", - DefaultPermissions = "AF", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-globe", @@ -1292,7 +1323,6 @@ internal class DatabaseDataCreator Key = Constants.Security.SensitiveDataGroupKey, Alias = Constants.Security.SensitiveDataGroupAlias, Name = "Sensitive data", - DefaultPermissions = string.Empty, CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-lock", diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 941c73010c..fdd1da6407 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -66,9 +66,9 @@ public class DatabaseSchemaCreator typeof(LockDto), typeof(UserGroupDto), typeof(User2UserGroupDto), - typeof(UserGroup2NodePermissionDto), typeof(UserGroup2AppDto), typeof(UserGroup2PermissionDto), + typeof(UserGroup2GranularPermissionDto), typeof(UserStartNodeDto), typeof(ContentNuDto), typeof(DocumentVersionDto), @@ -81,7 +81,6 @@ public class DatabaseSchemaCreator typeof(ContentScheduleDto), typeof(LogViewerQueryDto), typeof(ContentVersionCleanupPolicyDto), - typeof(UserGroup2NodeDto), typeof(CreatedPackageSchemaDto), typeof(UserGroup2LanguageDto), typeof(WebhookDto), diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 9d6fca5dd2..f394ae13b1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -73,5 +73,6 @@ public class UmbracoPlan : MigrationPlan To("{96525697-E9DC-4198-B136-25AD033442B8}"); To("{7FC5AC9B-6F56-415B-913E-4A900629B853}"); To("{1539A010-2EB5-4163-8518-4AE2AA98AFC6}"); + To("{C567DE81-DF92-4B99-BEA8-CD34EF99DA5D}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs index 32337f8f39..bc4b287eae 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs @@ -45,6 +45,9 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); AddColumnIfNotExists(columns, NewColumnName); + var nodeDtoTrashedIndex = $"IX_umbracoUserGroup_userGroupKey"; + CreateIndex(nodeDtoTrashedIndex); + // We want specific keys for the default user groups, so we need to fetch the user groups again to set their keys. List? userGroups = Database.Fetch(); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs index ab7dccffea..32cf861d19 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUsers.cs @@ -13,7 +13,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_0_0; /// This is an unscoped migration to support migrating sqlite, since it doesn't support adding columns. /// See for more information. /// -public class AddGuidsToUsers : UnscopedMigrationBase +internal class AddGuidsToUsers : UnscopedMigrationBase { private const string NewColumnName = "key"; private readonly IScopeProvider _scopeProvider; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateCharPermissionsToStrings.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateCharPermissionsToStrings.cs new file mode 100644 index 0000000000..4a2cd208d3 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/MigrateCharPermissionsToStrings.cs @@ -0,0 +1,114 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership.Permissions; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_0_0; + +[Obsolete("Remove in Umbraco 18.")] +internal class MigrateCharPermissionsToStrings : MigrationBase +{ + private readonly IIdKeyMap _idKeyMap; + + private static Dictionary> _charToStringPermissionDictionary = + new() + { + ['I'] = new []{ActionAssignDomain.ActionLetter}, + ['F'] = new []{ActionBrowse.ActionLetter}, + ['O'] = new []{ActionCopy.ActionLetter}, + ['ï'] = new []{ActionCreateBlueprintFromContent.ActionLetter}, + ['D'] = new []{ActionDelete.ActionLetter}, + ['M'] = new []{ActionMove.ActionLetter}, + ['C'] = new []{ActionNew.ActionLetter}, + ['N'] = new []{ActionNotify.ActionLetter}, + ['P'] = new []{ActionProtect.ActionLetter}, + ['U'] = new []{ActionPublish.ActionLetter}, + ['V'] = new []{ActionRestore.ActionLetter}, + ['R'] = new []{ActionRights.ActionLetter}, + ['K'] = new []{ActionRollback.ActionLetter}, + ['S'] = new []{ActionSort.ActionLetter}, + ['Z'] = new []{ActionUnpublish.ActionLetter}, + ['A'] = new []{ActionUpdate.ActionLetter}, + }; + + public MigrateCharPermissionsToStrings(IMigrationContext context, IIdKeyMap idKeyMap) + : base(context) + { + _idKeyMap = idKeyMap; + } + + protected override void Migrate() + { + if (TableExists(Constants.DatabaseSchema.Tables.UserGroup2GranularPermission)) + { + return; + } + + List? userGroups = Database.Fetch(); + + foreach (UserGroupDto userGroupDto in userGroups) + { + if (userGroupDto.DefaultPermissions == null) + { + continue; + } + + var permissions = userGroupDto.DefaultPermissions.SelectMany(oldPermission => + { + IEnumerable newPermissions = ReplacePermissionValue(oldPermission); + return newPermissions.Select(permission => new UserGroup2PermissionDto() + { + Permission = permission, UserGroupKey = userGroupDto.Key + }); + }).ToHashSet(); + + Database.InsertBulk(permissions); + + userGroupDto.DefaultPermissions = null; + + Database.Update(userGroupDto); + } + + Create.Table().Do(); + + + List? userGroup2NodePermissionDtos = Database.Fetch(); + + var userGroupIdToKeys = userGroups.ToDictionary(x => x.Id, x => x.Key); + IEnumerable userGroup2GranularPermissionDtos = userGroup2NodePermissionDtos.SelectMany(userGroup2NodePermissionDto => + { + HashSet permissions = userGroup2NodePermissionDto.Permission?.SelectMany(ReplacePermissionValue).ToHashSet() ?? new HashSet(); + + return permissions.Select(permission => + { + var uniqueIdAttempt = + _idKeyMap.GetKeyForId(userGroup2NodePermissionDto.NodeId, UmbracoObjectTypes.Document); + + if (uniqueIdAttempt.Success is false) + { + throw new InvalidOperationException("Did not find a key for the document id: " + + userGroup2NodePermissionDto.NodeId); + } + + return new UserGroup2GranularPermissionDto() + { + Permission = permission, + UserGroupKey = userGroupIdToKeys[userGroup2NodePermissionDto.UserGroupId], + UniqueId = uniqueIdAttempt.Result, + Context = DocumentGranularPermission.ContextType + }; + }); + }); + + Database.InsertBulk(userGroup2GranularPermissionDtos); + + Delete.Table(Constants.DatabaseSchema.Tables.UserGroup2NodePermission).Do(); + Delete.Table(Constants.DatabaseSchema.Tables.UserGroup2Node).Do(); + } + + private IEnumerable ReplacePermissionValue(char oldPermission) => _charToStringPermissionDictionary.TryGetValue(oldPermission, out IEnumerable? newPermission) ? newPermission : oldPermission.ToString().Yield(); +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2GranularPermissionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2GranularPermissionDto.cs new file mode 100644 index 0000000000..9a6ebe294c --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2GranularPermissionDto.cs @@ -0,0 +1,45 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.UserGroup2GranularPermission)] +[ExplicitColumns] +public class UserGroup2GranularPermissionDto +{ + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoUserGroup2GranularPermissionDto", AutoIncrement = true)] + public int Id { get; set; } + + [Column("userGroupKey")] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUserGroup2GranularPermissionDto_UserGroupKey_UniqueId", IncludeColumns = "uniqueId")] + [ForeignKey(typeof(UserGroupDto), Column = "key")] + public Guid UserGroupKey { get; set; } + + [Column("uniqueId")] + [ForeignKey(typeof(NodeDto), Column = "uniqueId")] + [NullSetting(NullSetting = NullSettings.Null)] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUserGroup2GranularPermissionDto_UniqueId")] + public Guid? UniqueId { get; set; } + + [Column("permission")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public required string Permission { get; set; } + + [Column("context")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public required string Context { get; set; } +} + + +// this is UserGroup2GranularPermissionDto + int ids +// it is used for handling legacy cases where we use int Ids +internal class UserGroup2GranularPermissionWithIdsDto : UserGroup2GranularPermissionDto +{ + [Column("entityId")] + public int EntityId { get; set; } + + [Column("userGroupId")] + public int UserGroupId { get; set; } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs index 54b66b8e22..3c26abea76 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; +[Obsolete("Will be removed in Umbraco 18.")] [TableName(TableName)] [ExplicitColumns] internal class UserGroup2NodeDto diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs index 94a8fd4361..d6c14c432b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; +[Obsolete("Will be removed in Umbraco 18.")] [TableName(Constants.DatabaseSchema.Tables.UserGroup2NodePermission)] [ExplicitColumns] internal class UserGroup2NodePermissionDto diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2PermissionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2PermissionDto.cs index c5cf3b12b7..98bb588719 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2PermissionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2PermissionDto.cs @@ -8,13 +8,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; [ExplicitColumns] public class UserGroup2PermissionDto { + [Column("id")] [PrimaryKeyColumn(Name = "PK_userGroup2Permission", AutoIncrement = true)] public int Id { get; set; } - [Column("userGroupId")] + [Column("userGroupKey")] [Index(IndexTypes.NonClustered, IncludeColumns = "permission")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } + [ForeignKey(typeof(UserGroupDto), Column = "key")] + public Guid UserGroupKey { get; set; } [Column("permission")] [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs index bd9803dfa0..548d2ff57d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs @@ -15,6 +15,7 @@ public class UserGroupDto UserGroup2AppDtos = new List(); UserGroup2LanguageDtos = new List(); UserGroup2PermissionDtos = new List(); + UserGroup2GranularPermissionDtos = new List(); } [Column("id")] @@ -40,6 +41,7 @@ public class UserGroupDto [Column("userGroupDefaultPermissions")] [Length(50)] [NullSetting(NullSetting = NullSettings.Null)] + [Obsolete("Is not used anymore Use UserGroup2PermissionDtos instead. This will be removed in Umbraco 18.")] public string? DefaultPermissions { get; set; } [Column("createDate")] @@ -82,6 +84,10 @@ public class UserGroupDto [Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")] public List UserGroup2PermissionDtos { get; set; } + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")] + public List UserGroup2GranularPermissionDtos { get; set; } + /// /// This is only relevant when this column is included in the results (i.e. GetUserGroupsWithUserCounts) /// diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs index ebedc57b35..e8eb1005a1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -1,13 +1,18 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Factories; internal static class UserFactory { - public static IUser BuildEntity(GlobalSettings globalSettings, UserDto dto) + public static IUser BuildEntity( + GlobalSettings globalSettings, + UserDto dto, + IDictionary permissionMappers) { Guid key = dto.Key; // This should only happen if the user is still not migrated to have a true key. @@ -18,7 +23,7 @@ internal static class UserFactory var user = new User(globalSettings, dto.Id, dto.UserName, dto.Email, dto.Login, dto.Password, dto.PasswordConfig, - dto.UserGroupDtos.Select(x => ToReadOnlyGroup(x)).ToArray(), + dto.UserGroupDtos.Select(x => ToReadOnlyGroup(x, permissionMappers)).ToArray(), dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content) .Select(x => x.StartNode).ToArray(), dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Media) @@ -115,12 +120,8 @@ internal static class UserFactory return dto; } - private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group) + private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group, IDictionary permissionMappers) { - IEnumerable permissions = group.DefaultPermissions is null - ? Enumerable.Empty() - : group.DefaultPermissions.ToCharArray().Select(x => x.ToString()); - return new ReadOnlyUserGroup( group.Id, group.Key, @@ -131,8 +132,20 @@ internal static class UserFactory group.Alias, group.UserGroup2LanguageDtos.Select(x => x.LanguageId), group.UserGroup2AppDtos.Select(x => x.AppAlias).WhereNotNull().ToArray(), - permissions, group.UserGroup2PermissionDtos.Select(x => x.Permission).ToHashSet(), + new HashSet(group.UserGroup2GranularPermissionDtos.Select(granularPermission => + { + if (permissionMappers.TryGetValue(granularPermission.Context, out IPermissionMapper? mapper)) + { + return mapper.MapFromDto(granularPermission); + } + + return new UnknownTypeGranularPermission() + { + Permission = granularPermission.Permission, + Context = granularPermission.Context + }; + })), group.HasAccessToAllLanguages); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs index a272f84fa6..5cc65e6a01 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs @@ -1,21 +1,22 @@ using System.Globalization; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Factories; internal static class UserGroupFactory { - public static IUserGroup BuildEntity(IShortStringHelper shortStringHelper, UserGroupDto dto) + public static IUserGroup BuildEntity(IShortStringHelper shortStringHelper, UserGroupDto dto, IDictionary permissionMappers) { var userGroup = new UserGroup( shortStringHelper, dto.UserCount, dto.Alias, dto.Name, - dto.DefaultPermissions.IsNullOrWhiteSpace() ? Enumerable.Empty() : dto.DefaultPermissions!.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList(), dto.Icon); try @@ -27,7 +28,7 @@ internal static class UserGroupFactory userGroup.UpdateDate = dto.UpdateDate; userGroup.StartContentId = dto.StartContentId; userGroup.StartMediaId = dto.StartMediaId; - userGroup.PermissionNames = dto.UserGroup2PermissionDtos.Select(x => x.Permission).ToHashSet(); + userGroup.Permissions = dto.UserGroup2PermissionDtos.Select(x => x.Permission).ToHashSet(); userGroup.HasAccessToAllLanguages = dto.HasAccessToAllLanguages; if (dto.UserGroup2AppDtos != null) { @@ -42,6 +43,31 @@ internal static class UserGroupFactory userGroup.AddAllowedLanguage(language.LanguageId); } + foreach (UserGroup2PermissionDto permission in dto.UserGroup2PermissionDtos) + { + userGroup.Permissions.Add(permission.Permission); + } + + foreach (UserGroup2GranularPermissionDto granularPermission in dto.UserGroup2GranularPermissionDtos) + { + IGranularPermission toInsert; + + if (permissionMappers.TryGetValue(granularPermission.Context, out var mapper)) + { + toInsert = mapper.MapFromDto(granularPermission); + } + else + { + toInsert = new UnknownTypeGranularPermission() + { + Permission = granularPermission.Permission, + Context = granularPermission.Context, + }; + } + + userGroup.GranularPermissions.Add(toInsert); + } + userGroup.ResetDirtyProperties(false); return userGroup; } @@ -57,7 +83,6 @@ internal static class UserGroupFactory { Key = entity.Key, Alias = entity.Alias, - DefaultPermissions = entity.Permissions == null ? string.Empty : string.Join(string.Empty, entity.Permissions), Name = entity.Name, UserGroup2AppDtos = new List(), CreateDate = entity.CreateDate, @@ -79,6 +104,17 @@ internal static class UserGroupFactory dto.UserGroup2AppDtos.Add(appDto); } + foreach (var permission in entity.Permissions) + { + var permissionDto = new UserGroup2PermissionDto { Permission = permission }; + if (entity.HasIdentity) + { + permissionDto.UserGroupKey = entity.Key; + } + + dto.UserGroup2PermissionDtos.Add(permissionDto); + } + if (entity.HasIdentity) { dto.Id = entity.Id; diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/IPermissionMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/IPermissionMapper.cs new file mode 100644 index 0000000000..c04b616e78 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/IPermissionMapper.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Models.Membership.Permissions; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public interface IPermissionMapper +{ + string Context { get; } + IGranularPermission MapFromDto(UserGroup2GranularPermissionDto dto); +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 5c51889488..c56c3e6934 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1533,8 +1533,8 @@ WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentT var list = new List { "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2Permission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", + "DELETE FROM umbracoUserGroup2GranularPermission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", "DELETE FROM cmsTagRelationship WHERE nodeId = @id", "DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @id", "DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @id", diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs index 28e9385811..5d10e5824b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -403,7 +403,7 @@ internal class DataTypeRepository : EntityRepositoryBase, IDataT Database.Delete("WHERE nodeId = @Id", new { entity.Id }); // Remove Permissions - Database.Delete("WHERE nodeId = @Id", new { entity.Id }); + Database.Delete("WHERE uniqueId = @Key", new { Key = entity.Key }); // Remove associated tags Database.Delete("WHERE nodeId = @Id", new { entity.Id }); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 210f5def2f..ce9c659720 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -717,8 +717,7 @@ public class DocumentRepository : ContentRepositoryBase /// /// - public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) => + public void AssignEntityPermission(IContent entity, string permission, IEnumerable groupIds) => PermissionRepository.AssignEntityPermission(entity, permission, groupIds); public EntityPermissionCollection GetPermissionsForEntity(int entityId) => diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index ff3c929463..36a5dd9063 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -313,6 +313,13 @@ internal class EntityRepository : RepositoryBase, IEntityRepositoryExtended return Database.ExecuteScalar(sql) > 0; } + public bool Exists(IEnumerable keys) + { + var distictKeys = keys.Distinct(); + Sql sql = Sql().SelectCount().From().Where(x => distictKeys.Contains(x.UniqueId)); + return Database.ExecuteScalar(sql) == distictKeys.Count(); + } + /// public bool Exists(Guid key, Guid objectType) { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index 73cb423837..744a55591f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -260,8 +260,7 @@ public class MediaRepository : ContentRepositoryBase { "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2GranularPermission + " WHERE uniqueId IN (SELECT uniqueId FROM umbracoNode WHERE id = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", "UPDATE " + Constants.DatabaseSchema.Tables.UserGroup + " SET startContentId = NULL WHERE startContentId = @id", diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs index 5dfd164f0d..96d797b057 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs @@ -170,11 +170,13 @@ internal class MemberGroupRepository : EntityRepositoryBase, var list = new[] { "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", - "DELETE FROM umbracoRelation WHERE parentId = @id", "DELETE FROM umbracoRelation WHERE childId = @id", + "DELETE FROM umbracoUserGroup2Permission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", + "DELETE FROM umbracoUserGroup2GranularPermission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", + "DELETE FROM umbracoRelation WHERE parentId = @id", + "DELETE FROM umbracoRelation WHERE childId = @id", "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM cmsMember2MemberGroup WHERE MemberGroup = @id", "DELETE FROM umbracoNode WHERE id = @id", + "DELETE FROM cmsMember2MemberGroup WHERE MemberGroup = @id", + "DELETE FROM umbracoNode WHERE id = @id", }; return list; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index c89344716f..c77f5bd684 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -540,8 +540,9 @@ public class MemberRepository : ContentRepositoryBase { "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + + "DELETE FROM umbracoUserGroup2Permission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", + "DELETE FROM umbracoUserGroup2GranularPermission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", "DELETE FROM umbracoRelation WHERE parentId = @id", "DELETE FROM umbracoRelation WHERE childId = @id", "DELETE FROM cmsTagRelationship WHERE nodeId = @id", diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs index 79be0f93b0..c99ac14fce 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; @@ -33,7 +34,7 @@ internal class PermissionRepository : EntityRepositoryBase /// Returns explicitly defined permissions for a user group for any number of nodes /// - /// + /// /// The group ids to lookup permissions for /// /// @@ -41,23 +42,25 @@ internal class PermissionRepository : EntityRepositoryBase /// This method will not support passing in more than 2000 group IDs when also passing in entity IDs. /// - public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, params int[] entityIds) + public EntityPermissionCollection GetPermissionsForEntities(int[] userGroupIds, params int[] entityIds) { var result = new EntityPermissionCollection(); if (entityIds.Length == 0) { - foreach (IEnumerable group in groupIds.InGroupsOf(Constants.Sql.MaxParameterCount)) + foreach (IEnumerable group in userGroupIds.InGroupsOf(Constants.Sql.MaxParameterCount)) { Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin().On( - (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => group.Contains(dto.UserGroupId)); + .Select("gp").AndSelect("ug.id as userGroupId, en.id as entityId") + .From("ug") + .InnerJoin("gp") + .On((left, right) => left.UserGroupKey == right.Key && group.Contains(right.Id), "gp", "ug") + .InnerJoin("en") + .On((left, right) => left.UniqueId == right.UniqueId, "gp", "en"); + + List permissions = + AmbientScope.Database.Fetch(sql); - List permissions = - AmbientScope.Database.Fetch(sql); foreach (EntityPermission permission in ConvertToPermissionList(permissions)) { result.Add(permission); @@ -66,19 +69,21 @@ internal class PermissionRepository : EntityRepositoryBase group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount - - groupIds.Length)) + foreach (IEnumerable entityGroup in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount - + userGroupIds.Length)) { Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin().On( - (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => - groupIds.Contains(dto.UserGroupId) && group.Contains(dto.NodeId)); + .Select("gp").AndSelect("ug.id as userGroupId, en.id as entityId") + .From("ug") + .InnerJoin("gp") + .On((left, right) => left.UserGroupKey == right.Key && userGroupIds.Contains(right.Id), "gp", "ug") + .InnerJoin("en") + .On((left, right) => left.UniqueId == right.UniqueId, "gp", "en") + .Where(en => entityGroup.Contains(en.NodeId), "en"); + + List permissions = + AmbientScope.Database.Fetch(sql); - List permissions = - AmbientScope.Database.Fetch(sql); foreach (EntityPermission permission in ConvertToPermissionList(permissions)) { result.Add(permission); @@ -97,16 +102,18 @@ internal class PermissionRepository : EntityRepositoryBase GetPermissionsForEntities(int[] entityIds) { Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin() - .On((left, right) => - left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => entityIds.Contains(dto.NodeId)) - .OrderBy(dto => dto.NodeId); + .Select("gp").AndSelect("ug.id as userGroupId, en.id as entityId") + .From("ug") + .InnerJoin("gp") + .On((left, right) => left.UserGroupKey == right.Key, "gp", "ug") + .InnerJoin("en") + .On((left, right) => left.UniqueId == right.UniqueId, "gp", "en") + .Where(en => entityIds.Contains(en.NodeId), "en"); - List result = AmbientScope.Database.Fetch(sql); - return ConvertToPermissionList(result); + List permissions = + AmbientScope.Database.Fetch(sql); + + return ConvertToPermissionList(permissions); } /// @@ -117,16 +124,18 @@ internal class PermissionRepository : EntityRepositoryBase sql = Sql() - .SelectAll() - .From() - .LeftJoin() - .On((left, right) => - left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => dto.NodeId == entityId) - .OrderBy(dto => dto.NodeId); + .Select("gp").AndSelect("ug.id as userGroupId, en.id as entityId") + .From("ug") + .InnerJoin("gp") + .On((left, right) => left.UserGroupKey == right.Key, "gp", "ug") + .InnerJoin("en") + .On((left, right) => left.UniqueId == right.UniqueId, "gp", "en") + .Where(en => entityId == en.NodeId, "en"); - List result = AmbientScope.Database.Fetch(sql); - return ConvertToPermissionList(result); + List permissions = + AmbientScope.Database.Fetch(sql); + + return ConvertToPermissionList(permissions); } /// @@ -138,7 +147,7 @@ internal class PermissionRepository : EntityRepositoryBase /// This will first clear the permissions for this user and entities and recreate them /// - public void ReplacePermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + public void ReplacePermissions(int groupId, ISet permissions, params int[] entityIds) { if (entityIds.Length == 0) { @@ -147,33 +156,37 @@ internal class PermissionRepository : EntityRepositoryBase group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", new { groupId, nodeIds = group }); + db.Execute( + Sql() + .Delete() + .WhereIn( + x => x.UniqueId, + Sql() + .Select() + .From() + .Where(x => entityIds.Contains(x.NodeId))) + .WhereIn( + x => x.UserGroupKey, + Sql() + .Select(x=>x.Key) + .From() + .Where(x => x.Id == groupId))); - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", new { groupId, nodeIds = group }); - } + // This is a poor man's solution to avoid breaking changes.. Sooner or later we should obsolete this method and take Guids as input. + Guid userGroupKey = db.Fetch(Sql().Select(x => x.Key).From() + .Where(x => x.Id == groupId)).SingleOrDefault(); + var idToKey = db.Fetch(Sql().Select().From() + .Where(x => entityIds.Contains(x.NodeId))).ToDictionary(x=>x.NodeId, x=>x.UniqueId); - if (permissions is not null) - { - var toInsert = new List(); - var toInsertPermissions = new List(); - - foreach (var e in entityIds) + IEnumerable toInsert = + from entityId in entityIds + from permission in permissions + select new UserGroup2GranularPermissionDto() { - toInsert.Add(new UserGroup2NodeDto { NodeId = e, UserGroupId = groupId }); - foreach (var p in permissions) - { - toInsertPermissions.Add(new UserGroup2NodePermissionDto - { - NodeId = e, Permission = p.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId, - }); - } - } + Permission = permission, UniqueId = idToKey[entityId], UserGroupKey = userGroupKey, Context = DocumentGranularPermission.ContextType + }; - db.BulkInsertRecords(toInsert); - db.BulkInsertRecords(toInsertPermissions); - } + db.InsertBulk(toInsert); } /// @@ -182,25 +195,42 @@ internal class PermissionRepository : EntityRepositoryBase /// /// - public void AssignPermission(int groupId, char permission, params int[] entityIds) + public void AssignPermission(int groupId, string permission, params int[] entityIds) { IUmbracoDatabase db = AmbientScope.Database; - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@entityIds)", new { groupId, entityIds }); db.Execute( - "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)", - new { groupId, permission = permission.ToString(CultureInfo.InvariantCulture), entityIds }); + Sql() + .Delete() + .Where(x => x.Permission == permission) + .WhereIn( + x => x.UniqueId, + Sql() + .Select() + .From() + .Where(x => entityIds.Contains(x.NodeId))) + .WhereIn( + x => x.UserGroupKey, + Sql() + .Select(x=>x.Key) + .From() + .Where(x => x.Id == groupId))); - UserGroup2NodeDto[] actionsPermissions = - entityIds.Select(id => new UserGroup2NodeDto { NodeId = id, UserGroupId = groupId }).ToArray(); + // This is a poor man's solution to avoid breaking changes.. Sooner or later we should obsolete this method and take Guids as input. + var userGroupKey = db.Fetch(Sql().Select(x => x.Key).From() + .Where(x => x.Id == groupId)).SingleOrDefault(); + var idToKey = db.Fetch(Sql().Select().From() + .Where(x => entityIds.Contains(x.NodeId))).ToDictionary(x=>x.NodeId, x=>x.UniqueId); - UserGroup2NodePermissionDto[] actions = entityIds.Select(id => new UserGroup2NodePermissionDto - { - NodeId = id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId, - }).ToArray(); + var toInsert = entityIds.Select(e => new UserGroup2GranularPermissionDto() + { + Permission = permission, + UniqueId = idToKey[e], + UserGroupKey = userGroupKey, + Context = DocumentGranularPermission.ContextType + }); - db.BulkInsertRecords(actions); - db.BulkInsertRecords(actionsPermissions); + db.InsertBulk(toInsert); } /// @@ -209,33 +239,32 @@ internal class PermissionRepository : EntityRepositoryBase /// /// - public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) + public void AssignEntityPermission(TEntity entity, string permission, IEnumerable groupIds) { IUmbracoDatabase db = AmbientScope.Database; - var groupIdsA = groupIds.ToArray(); - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId AND userGroupId in (@groupIds)", new { nodeId = entity.Id, groupIds = groupIdsA }); db.Execute( - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)", - new - { - nodeId = entity.Id, - permission = permission.ToString(CultureInfo.InvariantCulture), - groupIds = groupIdsA, - }); + Sql() + .Delete() + .Where(x => x.Permission == permission && x.UniqueId == entity.Key) + .WhereIn( + x => x.UserGroupKey, + Sql() + .Select(x=>x.Key) + .From() + .Where(x => groupIds.Contains(x.Id)))); - UserGroup2NodePermissionDto[] actionsPermissions = groupIdsA.Select(id => new UserGroup2NodePermissionDto + // This is a poor man's solution to avoid breaking changes.. Sooner or later we should obsolete this method and take Guids as input. + var idToKey = db.Fetch(Sql().Select().From() + .Where(x => groupIds.Contains(x.Id))).ToDictionary(x=>x.Id, x=>x.Key); + + var toInsert = groupIds.Select(x => new UserGroup2GranularPermissionDto() { - NodeId = entity.Id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = id, - }).ToArray(); + Permission = permission, UniqueId = entity.Key, UserGroupKey = idToKey[x], Context = DocumentGranularPermission.ContextType + }); - UserGroup2NodeDto[] actions = groupIdsA.Select(id => new UserGroup2NodeDto - { - NodeId = entity.Id, UserGroupId = id, - }).ToArray(); + db.InsertBulk(toInsert); - db.BulkInsertRecords(actions); - db.BulkInsertRecords(actionsPermissions); } /// @@ -250,30 +279,35 @@ internal class PermissionRepository : EntityRepositoryBase() + .WhereIn( + x => x.UniqueId, + Sql() + .Select(x => x.UniqueId) + .From() + .Where(x => x.NodeId == permissionSet.EntityId))); - var toInsert = new List(); - var toInsertPermissions = new List(); - foreach (EntityPermission entityPermission in permissionSet.PermissionsSet) - { - toInsert.Add(new UserGroup2NodeDto - { - NodeId = permissionSet.EntityId, UserGroupId = entityPermission.UserGroupId, - }); - foreach (var permission in entityPermission.AssignedPermissions) - { - toInsertPermissions.Add(new UserGroup2NodePermissionDto + // This is a poor man's solution to avoid breaking changes.. Sooner or later we should obsolete this method and take Guids as input. + var userGroupIds = permissionSet.PermissionsSet.Select(x => x.UserGroupId); + var entityKey = db.Fetch(Sql().Select(x => x.UniqueId).From() + .Where(x => x.NodeId == permissionSet.EntityId)).SingleOrDefault(); + var idToKey = db.Fetch(Sql().Select().From() + .Where(x => userGroupIds.Contains(x.Id))).ToDictionary(x=>x.Id, x=>x.Key); + + + var toInsert = permissionSet.PermissionsSet + .SelectMany(x => x.AssignedPermissions + .Select(p => new UserGroup2GranularPermissionDto() { - NodeId = permissionSet.EntityId, - Permission = permission, - UserGroupId = entityPermission.UserGroupId, - }); - } - } + Permission = p, + UniqueId = entityKey, + UserGroupKey = idToKey[x.UserGroupId], + Context = DocumentGranularPermission.ContextType + })); - db.BulkInsertRecords(toInsert); - db.BulkInsertRecords(toInsertPermissions); + db.InsertBulk(toInsert); } /// @@ -300,21 +334,22 @@ internal class PermissionRepository : EntityRepositoryBase result) + IEnumerable result) { var permissions = new EntityPermissionCollection(); - IEnumerable> nodePermissions = result.GroupBy(x => x.NodeId); - foreach (IGrouping np in nodePermissions) + IEnumerable> nodePermissions = result.GroupBy(x => x.EntityId).OrderBy(x=>x.Key); + foreach (IGrouping np in nodePermissions) { - IEnumerable> userGroupPermissions = + IEnumerable> userGroupPermissions = np.GroupBy(x => x.UserGroupId); - foreach (IGrouping permission in userGroupPermissions) + foreach (IGrouping permission in userGroupPermissions) { - var perms = permission.Select(x => x.Permission).Distinct().ToArray(); + var perms = permission.Select(x => x.Permission).Distinct().WhereNotNull().ToHashSet(); // perms can contain null if there are no permissions assigned, but the node is chosen in the UI. - permissions.Add(new EntityPermission(permission.Key, np.Key, perms.WhereNotNull().ToArray())); + permissions.Add(new EntityPermission(permission.Key, np.Key, perms)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs index 9e01320fdc..94f184c92d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs @@ -383,8 +383,6 @@ internal class TemplateRepository : EntityRepositoryBase, ITempl var list = new List { "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", "UPDATE " + Constants.DatabaseSchema.Tables.DocumentVersion + " SET templateId = NULL WHERE templateId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentType + " WHERE templateNodeId = @id", diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs index b00c1875c5..c277ac25fc 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -6,11 +6,13 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; +using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; @@ -25,18 +27,21 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG private readonly PermissionRepository _permissionRepository; private readonly IShortStringHelper _shortStringHelper; private readonly UserGroupWithUsersRepository _userGroupWithUsersRepository; + private readonly IDictionary _permissionMappers; public UserGroupRepository( IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, ILoggerFactory loggerFactory, - IShortStringHelper shortStringHelper) + IShortStringHelper shortStringHelper, + IEnumerable permissionMappers) : base(scopeAccessor, appCaches, logger) { _shortStringHelper = shortStringHelper; _userGroupWithUsersRepository = new UserGroupWithUsersRepository(this, scopeAccessor, appCaches, loggerFactory.CreateLogger()); _permissionRepository = new PermissionRepository(scopeAccessor, appCaches, loggerFactory.CreateLogger>()); + _permissionMappers = permissionMappers.ToDictionary(x => x.Context); } public IUserGroup? Get(string alias) @@ -86,7 +91,7 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG sql.Where($"umbracoUserGroup.id IN ({innerSql.SQL})"); AppendGroupBy(sql); - return Database.Fetch(sql).Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + return Database.Fetch(sql).Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x, _permissionMappers)); } public void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[]? userIds) => @@ -141,7 +146,7 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG // TODO: We could/should change the EntityPermissionsCollection into a KeyedCollection and they key could be // a struct of the nodeid + groupid so then we don't actually allocate this class just to check if it's not // going to be included in the result! - var defaultPermission = new EntityPermission(group.Id, nodeId, group.Permissions?.ToArray() ?? Array.Empty(), true); + var defaultPermission = new EntityPermission(group.Id, nodeId, group.Permissions ?? new HashSet(), true); // Since this is a hashset, this will not add anything that already exists by group/node combination result.Add(defaultPermission); @@ -161,7 +166,7 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG /// are removed. /// /// Specify the nodes to replace permissions for. - public void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) => + public void ReplaceGroupPermissions(int groupId, ISet permissions, params int[] entityIds) => _permissionRepository.ReplacePermissions(groupId, permissions, entityIds); /// @@ -170,7 +175,7 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG /// Id of group /// Permissions as enumerable list of /// Specify the nodes to replace permissions for - public void AssignGroupPermission(int groupId, char permission, params int[] entityIds) => + public void AssignGroupPermission(int groupId, string permission, params int[] entityIds) => _permissionRepository.AssignPermission(groupId, permission, entityIds); public static string GetByAliasCacheKey(string alias) => CacheKeys.UserGroupGetByAliasCacheKeyPrefix + alias; @@ -305,9 +310,10 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG } dto.UserGroup2LanguageDtos = GetUserGroupLanguages(id); - dto.UserGroup2PermissionDtos = GetUserGroupPermissions(id); + dto.UserGroup2PermissionDtos = GetUserGroupPermissions(dto.Key); + dto.UserGroup2GranularPermissionDtos = GetUserGroupGranularPermissions(dto.Key); - IUserGroup userGroup = UserGroupFactory.BuildEntity(_shortStringHelper, dto); + IUserGroup userGroup = UserGroupFactory.BuildEntity(_shortStringHelper, dto, _permissionMappers); return userGroup; } @@ -331,7 +337,7 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG AssignUserGroupOneToManyTables(ref dtos); - return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x, _permissionMappers)); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -347,21 +353,26 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG AssignUserGroupOneToManyTables(ref dtos); - return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x, _permissionMappers)); } private void AssignUserGroupOneToManyTables(ref List userGroupDtos) { IDictionary> userGroups2Languages = GetAllUserGroupLanguageGrouped(); - IDictionary> userGroups2Permissions = GetAllUserGroupPermissionsGrouped(); + IDictionary> userGroups2Permissions = GetAllUserGroupPermissionsGrouped(); + IDictionary> userGroup2GranularPermissions = GetAllUserGroupGranularPermissionsGrouped(); foreach (UserGroupDto dto in userGroupDtos) { userGroups2Languages.TryGetValue(dto.Id, out List? userGroup2LanguageDtos); dto.UserGroup2LanguageDtos = userGroup2LanguageDtos ?? new List(); - userGroups2Permissions.TryGetValue(dto.Id, out List? userGroup2PermissionDtos); + userGroups2Permissions.TryGetValue(dto.Key, out List? userGroup2PermissionDtos); dto.UserGroup2PermissionDtos = userGroup2PermissionDtos ?? new List(); + + userGroup2GranularPermissions.TryGetValue(dto.Key, out List? userGroup2GranularPermissionDtos); + dto.UserGroup2GranularPermissionDtos = userGroup2GranularPermissionDtos ?? new List(); + } } @@ -420,10 +431,11 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG x => x.StartMediaId, x => x.UpdateDate, x => x.Alias, - x => x.DefaultPermissions, x => x.Name, x => x.HasAccessToAllLanguages, - x => x.Key) + x => x.Key, + x => x.DefaultPermissions + ) .AndBy(x => x.AppAlias, x => x.UserGroupId); protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.UserGroup}.id = @id"; @@ -434,9 +446,9 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG { "DELETE FROM umbracoUser2UserGroup WHERE userGroupId = @id", "DELETE FROM umbracoUserGroup2App WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2Permission WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup2Permission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", + "DELETE FROM umbracoUserGroup2GranularPermission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", + "DELETE FROM umbracoUserGroup2GranularPermission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", "DELETE FROM umbracoUserGroup WHERE id = @id", }; return list; @@ -454,6 +466,7 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG PersistAllowedSections(entity); PersistAllowedLanguages(entity); PersistPermissions(entity); + PersistGranularPermissions(entity); entity.ResetDirtyProperties(); } @@ -469,6 +482,7 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG PersistAllowedSections(entity); PersistAllowedLanguages(entity); PersistPermissions(entity); + PersistGranularPermissions(entity); entity.ResetDirtyProperties(); } @@ -510,14 +524,35 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG private void PersistPermissions(IUserGroup userGroup) { - Database.Delete("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id }); + Database.Delete("WHERE userGroupKey = @UserGroupKey", new { UserGroupKey = userGroup.Key }); - foreach (var permission in userGroup.PermissionNames) - { - var permissionDto = new UserGroup2PermissionDto { UserGroupId = userGroup.Id, Permission = permission, }; - Database.Insert(permissionDto); - } + IEnumerable permissionDtos = userGroup.Permissions + .Select(permission => new UserGroup2PermissionDto { UserGroupKey = userGroup.Key, Permission = permission }); + + Database.InsertBulk(permissionDtos); } + private void PersistGranularPermissions(IUserGroup userGroup) + { + Database.Delete("WHERE userGroupKey = @UserGroupKey", new { UserGroupKey = userGroup.Key }); + + IEnumerable permissionDtos = userGroup.GranularPermissions + .Select(permission => + { + var dto = new UserGroup2GranularPermissionDto + { + UserGroupKey = userGroup.Key, Permission = permission.Permission, Context = permission.Context + }; + if (permission is INodeGranularPermission nodeGranularPermission) + { + dto.UniqueId = nodeGranularPermission.Key; + } + + return dto; + }); + + Database.InsertBulk(permissionDtos); + } + private List GetUserGroupLanguages(int userGroupId) { @@ -537,25 +572,45 @@ public class UserGroupRepository : EntityRepositoryBase, IUserG return userGroupLanguages.GroupBy(x => x.UserGroupId).ToDictionary(x => x.Key, x => x.ToList()); } - private List GetUserGroupPermissions(int userGroupId) + private List GetUserGroupPermissions(Guid userGroupKey) { Sql query = Sql() .Select() .From() - .Where(x => x.UserGroupId == userGroupId); + .Where(x => x.UserGroupKey == userGroupKey); return Database.Fetch(query); } + private List GetUserGroupGranularPermissions(Guid userGroupKey) + { + Sql query = Sql() + .Select() + .From() + .Where(x => x.UserGroupKey == userGroupKey); - private Dictionary> GetAllUserGroupPermissionsGrouped() + return Database.Fetch(query); + } + + private Dictionary> GetAllUserGroupPermissionsGrouped() { Sql query = Sql() .Select() .From(); List userGroupPermissions = Database.Fetch(query); - return userGroupPermissions.GroupBy(x => x.UserGroupId).ToDictionary(x => x.Key, x => x.ToList()); + return userGroupPermissions.GroupBy(x => x.UserGroupKey).ToDictionary(x => x.Key, x => x.ToList()); } + private Dictionary> GetAllUserGroupGranularPermissionsGrouped() + { + Sql query = Sql() + .Select() + .From(); + + List userGroupGranularPermissions = Database.Fetch(query); + return userGroupGranularPermissions.GroupBy(x => x.UserGroupKey).ToDictionary(x => x.Key, x => x.ToList()); + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 5a3a95cbd9..0b78a30f0f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using System.Linq.Expressions; using System.Reflection; using System.Text; @@ -34,6 +35,7 @@ internal class UserRepository : EntityRepositoryBase, IUserRepositor private string? _passwordConfigJson; private bool _passwordConfigInitialized; private readonly object _sqliteValidateSessionLock = new(); + private readonly IDictionary _permissionMappers; /// /// Initializes a new instance of the class. @@ -49,6 +51,7 @@ internal class UserRepository : EntityRepositoryBase, IUserRepositor /// The password configuration. /// The JSON serializer. /// State of the runtime. + /// The permission mappers. /// /// mapperCollection /// or @@ -64,7 +67,8 @@ internal class UserRepository : EntityRepositoryBase, IUserRepositor IOptions globalSettings, IOptions passwordConfiguration, IJsonSerializer jsonSerializer, - IRuntimeState runtimeState) + IRuntimeState runtimeState, + IEnumerable permissionMappers) : base(scopeAccessor, appCaches, logger) { _mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection)); @@ -73,6 +77,7 @@ internal class UserRepository : EntityRepositoryBase, IUserRepositor passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); _jsonSerializer = jsonSerializer; _runtimeState = runtimeState; + _permissionMappers = permissionMappers.ToDictionary(x => x.Context); } /// @@ -99,7 +104,7 @@ internal class UserRepository : EntityRepositoryBase, IUserRepositor } private IEnumerable ConvertFromDtos(IEnumerable dtos) => - dtos.Select(x => UserFactory.BuildEntity(_globalSettings, x)); + dtos.Select(x => UserFactory.BuildEntity(_globalSettings, x, _permissionMappers)); #region Overrides of RepositoryBase @@ -137,7 +142,7 @@ internal class UserRepository : EntityRepositoryBase, IUserRepositor } PerformGetReferencedDtos(dtos); - return UserFactory.BuildEntity(_globalSettings, dtos[0]); + return UserFactory.BuildEntity(_globalSettings, dtos[0], _permissionMappers); } /// @@ -194,7 +199,7 @@ internal class UserRepository : EntityRepositoryBase, IUserRepositor PerformGetReferencedDtos(new List { userDto }); - return UserFactory.BuildEntity(_globalSettings, userDto); + return UserFactory.BuildEntity(_globalSettings, userDto, _permissionMappers); } /// @@ -349,7 +354,7 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 var i = 0; foreach (UserDto dto in dtos) { - users[i++] = UserFactory.BuildEntity(_globalSettings, dto); + users[i++] = UserFactory.BuildEntity(_globalSettings, dto, _permissionMappers); } return users; @@ -365,7 +370,7 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 var i = 0; foreach (UserDto dto in dtos) { - users[i++] = UserFactory.BuildEntity(_globalSettings, dto); + users[i++] = UserFactory.BuildEntity(_globalSettings, dto, _permissionMappers); } return users; @@ -374,7 +379,7 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 private IUser? GetWith(Action> with, bool includeReferences) { UserDto? dto = GetDtoWith(with, includeReferences); - return dto == null ? null : UserFactory.BuildEntity(_globalSettings, dto); + return dto == null ? null : UserFactory.BuildEntity(_globalSettings, dto, _permissionMappers); } private UserDto? GetDtoWith(Action> with, bool includeReferences) @@ -415,15 +420,48 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 List userIds = dtos.Count == 1 ? new List {dtos[0].Id} : dtos.Select(x => x.Id).ToList(); Dictionary? xUsers = dtos.Count == 1 ? null : dtos.ToDictionary(x => x.Id, x => x); - // get users2groups + List groupIds = new List(); + List groupKeys = new List(); + Sql sql; + try + { + sql = SqlContext.Sql() + .Select(x=>x.Id, x=>x.Key) + .From() + .InnerJoin().On((left, right) => left.Id == right.UserGroupId) + .WhereIn(x => x.UserId, userIds); - Sql sql = SqlContext.Sql() + List? userGroups = Database.Fetch(sql); + + + groupKeys= userGroups.Select(x => x.Key).ToList(); + + } + catch (DbException e) + { + // ignore doing upgrade, as we know the Key potentially do not exists + if (_runtimeState.Level != RuntimeLevel.Upgrade) + { + throw; + } + + } + + + // get users2groups + sql = SqlContext.Sql() .Select() .From() .WhereIn(x => x.UserId, userIds); List? user2Groups = Database.Fetch(sql); - var groupIds = user2Groups.Select(x => x.UserGroupId).ToList(); + + if (groupIds.Any() is false) + { + //this can happen if we are upgrading, so we try do read from this table, as we counn't because of the key earlier + groupIds = user2Groups.Select(x=>x.UserGroupId).Distinct().ToList(); + } + // get groups // We wrap this in a try-catch, as this might throw errors when you try to login before having migrated your database @@ -495,21 +533,43 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 sql = SqlContext.Sql() .Select() .From() - .WhereIn(x => x.UserGroupId, groupIds); + .WhereIn(x => x.UserGroupKey, groupKeys); - Dictionary> groups2permissions; + Dictionary> groups2permissions; try { groups2permissions = Database.Fetch(sql) - .GroupBy(x => x.UserGroupId) + .GroupBy(x => x.UserGroupKey) .ToDictionary(x => x.Key, x => x); } catch { // If we get an error, the table has not been made in the database yet, set the list to an empty one - groups2permissions = new Dictionary>(); + groups2permissions = new Dictionary>(); } + // get groups2granularPermissions + + sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserGroupKey, groupKeys); + + + Dictionary> groups2GranularPermissions; + try + { + groups2GranularPermissions = Database.Fetch(sql) + .GroupBy(x => x.UserGroupKey) + .ToDictionary(x => x.Key, x => x); + } + catch + { + // If we get an error, the table has not been made in the database yet, set the list to an empty one + groups2GranularPermissions = new Dictionary>(); + } + + // map groups foreach (User2UserGroupDto? user2Group in user2Groups) @@ -540,6 +600,8 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 } + + // map languages foreach (var group in groups.Values) @@ -553,12 +615,22 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 // map group permissions foreach (UserGroupDto? group in groups.Values) { - if (groups2permissions.TryGetValue(group.Id, out IGrouping? list)) + if (groups2permissions.TryGetValue(group.Key, out IGrouping? list)) { group.UserGroup2PermissionDtos = list.ToList(); // groups2apps is distinct } - } + + // map granular permissions + + foreach (UserGroupDto? group in groups.Values) + { + if (groups2GranularPermissions.TryGetValue(group.Key, out IGrouping? list)) + { + group.UserGroup2GranularPermissionDtos = list.ToList(); // groups2apps is distinct + } + } + } #endregion @@ -1059,7 +1131,7 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 // map references PerformGetReferencedDtos(pagedResult.Items); - return pagedResult.Items.Select(x => UserFactory.BuildEntity(_globalSettings, x)); + return pagedResult.Items.Select(x => UserFactory.BuildEntity(_globalSettings, x, _permissionMappers)); } private Sql ApplyFilter(Sql sql, Sql? filterSql, bool hasWhereClause) diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 5a97ee377b..bc284b6b8f 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -325,6 +325,7 @@ public class BackOfficeUserStore : throw; } + } /// diff --git a/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs b/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs index c5216f2cc8..97288789b7 100644 --- a/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs @@ -31,7 +31,7 @@ public class UserGroupBuilder private string _icon; private int? _id; private string _name; - private IEnumerable _permissions = Enumerable.Empty(); + private ISet _permissions = new HashSet(); private int? _startContentId; private int? _startMediaId; private string _suffix; @@ -83,18 +83,13 @@ public class UserGroupBuilder return this; } - public UserGroupBuilder WithPermissions(string permissions) - { - _permissions = permissions.ToCharArray().Select(x => x.ToString()); - return this; - } - - public UserGroupBuilder WithPermissions(IList permissions) + public UserGroupBuilder WithPermissions(ISet permissions) { _permissions = permissions; return this; } + public UserGroupBuilder WithAllowedSections(IList allowedSections) { _allowedSections = allowedSections; @@ -136,13 +131,15 @@ public class UserGroupBuilder var shortStringHelper = new DefaultShortStringHelper(new DefaultShortStringHelperConfig()); - var userGroup = new UserGroup(shortStringHelper, userCount, alias, name, _permissions, icon) + var userGroup = new UserGroup(shortStringHelper, userCount, alias, name, icon) { Id = id, StartContentId = startContentId, StartMediaId = startMediaId }; + userGroup.Permissions = _permissions; + foreach (var section in _allowedSections) { userGroup.AddAllowedSection(section); @@ -155,12 +152,12 @@ public class UserGroupBuilder string alias = "testGroup", string name = "Test Group", string suffix = "", - string[] permissions = null, + ISet? permissions = null, string[] allowedSections = null) => (UserGroup)new UserGroupBuilder() .WithAlias(alias + suffix) .WithName(name + suffix) - .WithPermissions(permissions ?? new[] { "A", "B", "C" }) + .WithPermissions(permissions ?? new[] { "A", "B", "C" }.ToHashSet()) .WithAllowedSections(allowedSections ?? new[] { "content", "media" }) .Build(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UserModelMapperTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UserModelMapperTests.cs index fb3773e1d0..25b5bd786c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UserModelMapperTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UserModelMapperTests.cs @@ -26,12 +26,10 @@ public class UserModelMapperTests : UmbracoIntegrationTest public void Map_UserGroupSave_To_IUserGroup() { IUserGroup userGroup = - new UserGroup(ShortStringHelper, 0, "alias", "name", new List { "c" }, "icon") { Id = 42 }; + new UserGroup(ShortStringHelper, 0, "alias", "name", "icon") { Id = 42 }; - // userGroup.permissions is List`1[System.String] + userGroup.Permissions = new HashSet() { "c" }; - // userGroup.permissions is System.Linq.Enumerable+WhereSelectArrayIterator`2[System.Char, System.String] - // fixed: now List`1[System.String] const string json = "{\"id\":@@@ID@@@,\"alias\":\"perm1\",\"name\":\"Perm1\",\"icon\":\"icon-users\",\"sections\":[\"content\"],\"users\":[],\"defaultPermissions\":[\"F\",\"C\",\"A\"],\"assignedPermissions\":{},\"startContentId\":-1,\"startMediaId\":-1,\"action\":\"save\",\"parentId\":-1}"; var userGroupSave = diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index a52b2385be..21bffb23aa 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -1934,7 +1934,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ContentService.Save(childPage); // assign explicit permissions to the child - ContentService.SetPermission(childPage, 'A', new[] { userGroup.Id }); + ContentService.SetPermission(childPage, "A", new[] { userGroup.Id }); // Ok, now copy, what should happen is the childPage will retain it's own permissions var parentPage2 = ContentBuilder.CreateSimpleContent(contentType); @@ -1970,7 +1970,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var parentPage = ContentBuilder.CreateSimpleContent(contentType); ContentService.Save(parentPage); - ContentService.SetPermission(parentPage, 'A', new[] { userGroup.Id }); + ContentService.SetPermission(parentPage, "A", new[] { userGroup.Id }); var childPage1 = ContentBuilder.CreateSimpleContent(contentType, "child1", parentPage); ContentService.Save(childPage1); @@ -2002,7 +2002,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // create a new parent with a new permission structure var parentPage2 = ContentBuilder.CreateSimpleContent(contentType); ContentService.Save(parentPage2); - ContentService.SetPermission(parentPage2, 'B', new[] { userGroup.Id }); + ContentService.SetPermission(parentPage2, "B", new[] { userGroup.Id }); // Now copy, what should happen is the child pages will now have permissions inherited from the new parent var copy = ContentService.Copy(childPage1, parentPage2.Id, false, true); @@ -2075,7 +2075,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var userGroup = UserService.GetUserGroupByAlias(user.Groups.First().Alias); Assert.IsNotNull(NotificationService.CreateNotification(user, content1, "X")); - ContentService.SetPermission(content1, 'A', new[] { userGroup.Id }); + ContentService.SetPermission(content1, "A", new[] { userGroup.Id }); var updateDomainResult = await DomainService.UpdateDomainsAsync( content1.Key, new DomainsUpdateModel diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserGroupRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserGroupRepositoryTest.cs index e095452e24..1a7ec16d25 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserGroupRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserGroupRepositoryTest.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common.Builders; @@ -19,8 +20,11 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] public class UserGroupRepositoryTest : UmbracoIntegrationTest { + private IEnumerable PermissionMappers => GetRequiredService>(); + + private UserGroupRepository CreateRepository(IScopeProvider provider) => - new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), LoggerFactory, ShortStringHelper); + new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), LoggerFactory, ShortStringHelper, PermissionMappers); [Test] public void Can_Perform_Add_On_UserGroupRepository() @@ -103,7 +107,7 @@ public class UserGroupRepositoryTest : UmbracoIntegrationTest // Act var resolved = repository.Get(userGroup.Id); resolved.Name = "New Name"; - resolved.Permissions = new[] { "Z", "Y", "X" }; + resolved.Permissions = new[] { "Z", "Y", "X" }.ToHashSet(); repository.Save(resolved); scope.Complete(); var updatedItem = repository.Get(userGroup.Id); @@ -130,7 +134,7 @@ public class UserGroupRepositoryTest : UmbracoIntegrationTest var id = userGroup.Id; - var repository2 = new UserGroupRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), LoggerFactory, ShortStringHelper); + var repository2 = new UserGroupRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), LoggerFactory, ShortStringHelper, PermissionMappers); repository2.Delete(userGroup); scope.Complete(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs index 58a1c73019..cce400878a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs @@ -38,6 +38,7 @@ public class UserRepositoryTest : UmbracoIntegrationTest private IMediaTypeRepository MediaTypeRepository => GetRequiredService(); private IMediaRepository MediaRepository => GetRequiredService(); + private IEnumerable PermissionMappers => GetRequiredService>(); private UserRepository CreateRepository(ICoreScopeProvider provider) { @@ -52,7 +53,8 @@ public class UserRepositoryTest : UmbracoIntegrationTest Options.Create(GlobalSettings), Options.Create(new UserPasswordConfigurationSettings()), new SystemTextJsonSerializer(), - mockRuntimeState.Object); + mockRuntimeState.Object, + PermissionMappers); return repository; } @@ -66,7 +68,7 @@ public class UserRepositoryTest : UmbracoIntegrationTest private UserGroupRepository CreateUserGroupRepository(ICoreScopeProvider provider) { var accessor = (IScopeAccessor)provider; - return new UserGroupRepository(accessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), LoggerFactory, ShortStringHelper); + return new UserGroupRepository(accessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), LoggerFactory, ShortStringHelper, PermissionMappers); } [Test] @@ -158,7 +160,8 @@ public class UserRepositoryTest : UmbracoIntegrationTest Options.Create(GlobalSettings), Options.Create(new UserPasswordConfigurationSettings()), new SystemTextJsonSerializer(), - mockRuntimeState.Object); + mockRuntimeState.Object, + PermissionMappers); repository2.Delete(user); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs index 8884622bf2..67048e2900 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/UserServiceTests.cs @@ -58,9 +58,9 @@ public class UserServiceTests : UmbracoIntegrationTest // Assert Assert.AreEqual(3, permissions.Length); - Assert.AreEqual(17, permissions[0].AssignedPermissions.Length); - Assert.AreEqual(17, permissions[1].AssignedPermissions.Length); - Assert.AreEqual(17, permissions[2].AssignedPermissions.Length); + Assert.AreEqual(17, permissions[0].AssignedPermissions.Count); + Assert.AreEqual(17, permissions[1].AssignedPermissions.Count); + Assert.AreEqual(17, permissions[2].AssignedPermissions.Count); } [Test] @@ -88,13 +88,13 @@ public class UserServiceTests : UmbracoIntegrationTest ContentService.SetPermission(content[2], ActionBrowse.ActionLetter, new[] { userGroup.Id }); // Act - var permissions = UserService.GetPermissions(user, content[0].Id, content[1].Id, content[2].Id).ToArray(); + var permissions = UserService.GetPermissions(user, content[0].Id, content[1].Id, content[2].Id).OrderBy(x=>x.EntityId).ToArray(); // Assert Assert.AreEqual(3, permissions.Length); - Assert.AreEqual(3, permissions[0].AssignedPermissions.Length); - Assert.AreEqual(2, permissions[1].AssignedPermissions.Length); - Assert.AreEqual(1, permissions[2].AssignedPermissions.Length); + Assert.AreEqual(3, permissions[0].AssignedPermissions.Count); + Assert.AreEqual(2, permissions[1].AssignedPermissions.Count); + Assert.AreEqual(1, permissions[2].AssignedPermissions.Count); } [Test] @@ -127,9 +127,9 @@ public class UserServiceTests : UmbracoIntegrationTest // Assert Assert.AreEqual(3, permissions.Length); - Assert.AreEqual(3, permissions[0].AssignedPermissions.Length); - Assert.AreEqual(2, permissions[1].AssignedPermissions.Length); - Assert.AreEqual(1, permissions[2].AssignedPermissions.Length); + Assert.AreEqual(3, permissions[0].AssignedPermissions.Count); + Assert.AreEqual(2, permissions[1].AssignedPermissions.Count); + Assert.AreEqual(1, permissions[2].AssignedPermissions.Count); } [Test] @@ -161,9 +161,9 @@ public class UserServiceTests : UmbracoIntegrationTest // Assert Assert.AreEqual(3, permissions.Length); - Assert.AreEqual(3, permissions[0].AssignedPermissions.Length); - Assert.AreEqual(2, permissions[1].AssignedPermissions.Length); - Assert.AreEqual(17, permissions[2].AssignedPermissions.Length); + Assert.AreEqual(3, permissions[0].AssignedPermissions.Count); + Assert.AreEqual(2, permissions[1].AssignedPermissions.Count); + Assert.AreEqual(17, permissions[2].AssignedPermissions.Count); } [Test] @@ -310,19 +310,19 @@ public class UserServiceTests : UmbracoIntegrationTest const int groupB = 8; const int groupC = 9; - var userGroups = new Dictionary + var userGroups = new Dictionary> { - {groupA, new[] {"S", "D", "F"}}, {groupB, new[] {"S", "D", "G", "K"}}, {groupC, new[] {"F", "G"}} + {groupA, new[] {"S", "D", "F"}.ToHashSet()}, {groupB, new[] {"S", "D", "G", "K"}.ToHashSet()}, {groupC, new[] {"F", "G"}.ToHashSet()} }; EntityPermission[] permissions = { new(groupA, 1, userGroups[groupA], true), new(groupA, 2, userGroups[groupA], true), new(groupA, 3, userGroups[groupA], true), new(groupA, 4, userGroups[groupA], true), - new(groupB, 1, userGroups[groupB], true), new(groupB, 2, new[] {"F", "R"}, false), + new(groupB, 1, userGroups[groupB], true), new(groupB, 2, new[] {"F", "R"}.ToHashSet(), false), new(groupB, 3, userGroups[groupB], true), new(groupB, 4, userGroups[groupB], true), new(groupC, 1, userGroups[groupC], true), new(groupC, 2, userGroups[groupC], true), - new(groupC, 3, new[] {"Q", "Z"}, false), new(groupC, 4, userGroups[groupC], true) + new(groupC, 3, new[] {"Q", "Z"}.ToHashSet(), false), new(groupC, 4, userGroups[groupC], true) }; // Permissions for Id 4 @@ -359,13 +359,13 @@ public class UserServiceTests : UmbracoIntegrationTest { var path = "-1,1,2,3"; var pathIds = path.GetIdsFromPathReversed(); - string[] defaults = { "A", "B" }; + ISet defaults = new []{ "A", "B" }.ToHashSet(); var permissions = new List { - new(9876, 1, defaults, true), new(9876, 2, new[] {"B", "C", "D"}, false), new(9876, 3, defaults, true) + new(9876, 1, defaults, true), new(9876, 2, new[] {"B", "C", "D"}.ToHashSet(), false), new(9876, 3, defaults, true) }; var result = UserService.GetPermissionsForPathForGroup(permissions, pathIds, true); - Assert.AreEqual(3, result.AssignedPermissions.Length); + Assert.AreEqual(3, result.AssignedPermissions.Count); Assert.IsFalse(result.IsDefaultPermissions); Assert.IsTrue(result.AssignedPermissions.ContainsAll(new[] { "B", "C", "D" })); Assert.AreEqual(2, result.EntityId); @@ -377,7 +377,7 @@ public class UserServiceTests : UmbracoIntegrationTest { var path = "-1,1,2,3"; var pathIds = path.GetIdsFromPathReversed(); - string[] defaults = { "A", "B", "C" }; + ISet defaults = new []{ "A", "B", "C" }.ToHashSet(); var permissions = new List { new(9876, 1, defaults, true), new(9876, 2, defaults, true), new(9876, 3, defaults, true) @@ -391,13 +391,13 @@ public class UserServiceTests : UmbracoIntegrationTest { var path = "-1,1,2,3"; var pathIds = path.GetIdsFromPathReversed(); - string[] defaults = { "A", "B" }; + ISet defaults = new []{ "A", "B" }.ToHashSet(); var permissions = new List { new(9876, 1, defaults, true), new(9876, 2, defaults, true), new(9876, 3, defaults, true) }; var result = UserService.GetPermissionsForPathForGroup(permissions, pathIds, true); - Assert.AreEqual(2, result.AssignedPermissions.Length); + Assert.AreEqual(2, result.AssignedPermissions.Count); Assert.IsTrue(result.IsDefaultPermissions); Assert.IsTrue(result.AssignedPermissions.ContainsAll(defaults)); Assert.AreEqual(3, result.EntityId); @@ -1021,7 +1021,7 @@ public class UserServiceTests : UmbracoIntegrationTest private UserGroup CreateTestUserGroup(string alias = "testGroup", string name = "Test Group") { - var permissions = "ABCDEFGHIJ1234567".ToCharArray().Select(x => x.ToString()).ToArray(); + var permissions = "ABCDEFGHIJ1234567".ToCharArray().Select(x => x.ToString()).ToHashSet(); var userGroup = UserGroupBuilder.CreateUserGroup(alias, name, permissions: permissions); UserService.Save(userGroup); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Security/ContentPermissionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Security/ContentPermissionsTests.cs index a7d679d7ab..ff945e78bc 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Security/ContentPermissionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Security/ContentPermissionsTests.cs @@ -62,7 +62,7 @@ public class ContentPermissionsTests var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act - var result = contentPermissions.CheckPermissions(1234, user, out IContent _, new[] { 'F' }); + var result = contentPermissions.CheckPermissions(1234, user, out IContent _, new[] { "F" }.ToHashSet()); // Assert Assert.AreEqual(ContentPermissions.ContentAccess.NotFound, result); @@ -91,7 +91,7 @@ public class ContentPermissionsTests var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act - var result = contentPermissions.CheckPermissions(1234, user, out IContent _, new[] { 'F' }); + var result = contentPermissions.CheckPermissions(1234, user, out IContent _, new[] { "F" }.ToHashSet()); // Assert Assert.AreEqual(ContentPermissions.ContentAccess.Denied, result); @@ -109,7 +109,7 @@ public class ContentPermissionsTests contentServiceMock.Setup(x => x.GetById(1234)).Returns(content); var contentService = contentServiceMock.Object; var userServiceMock = new Mock(); - var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A", "B", "C" }) }; + var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A", "B", "C" }.ToHashSet()) }; var permissionSet = new EntityPermissionSet(1234, permissions); userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1,1234,5678")).Returns(permissionSet); var userService = userServiceMock.Object; @@ -118,7 +118,7 @@ public class ContentPermissionsTests var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act - var result = contentPermissions.CheckPermissions(1234, user, out IContent _, new[] { 'F' }); + var result = contentPermissions.CheckPermissions(1234, user, out IContent _, new[] { "F" }.ToHashSet()); // Assert Assert.AreEqual(ContentPermissions.ContentAccess.Denied, result); @@ -135,7 +135,7 @@ public class ContentPermissionsTests var contentServiceMock = new Mock(); contentServiceMock.Setup(x => x.GetById(1234)).Returns(content); var contentService = contentServiceMock.Object; - var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A", "F", "C" }) }; + var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A", "F", "C" }.ToHashSet()) }; var permissionSet = new EntityPermissionSet(1234, permissions); var userServiceMock = new Mock(); userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1,1234,5678")).Returns(permissionSet); @@ -145,7 +145,7 @@ public class ContentPermissionsTests var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act - var result = contentPermissions.CheckPermissions(1234, user, out IContent _, new[] { 'F' }); + var result = contentPermissions.CheckPermissions(1234, user, out IContent _, new[] { "F" }.ToHashSet()); // Assert Assert.AreEqual(ContentPermissions.ContentAccess.Granted, result); @@ -243,7 +243,7 @@ public class ContentPermissionsTests var user = CreateUser(); var userServiceMock = new Mock(); - var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A" }) }; + var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A" }.ToHashSet()) }; var permissionSet = new EntityPermissionSet(1234, permissions); userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1")).Returns(permissionSet); var contentServiceMock = new Mock(); @@ -254,7 +254,7 @@ public class ContentPermissionsTests var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act - var result = contentPermissions.CheckPermissions(-1, user, out IContent _, new[] { 'A' }); + var result = contentPermissions.CheckPermissions(-1, user, out IContent _, new[] { "A" }.ToHashSet()); // Assert Assert.AreEqual(ContentPermissions.ContentAccess.Granted, result); @@ -267,7 +267,7 @@ public class ContentPermissionsTests var user = CreateUser(withUserGroup: false); var userServiceMock = new Mock(); - var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A" }) }; + var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A" }.ToHashSet()) }; var permissionSet = new EntityPermissionSet(1234, permissions); userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-1")).Returns(permissionSet); var userService = userServiceMock.Object; @@ -278,7 +278,7 @@ public class ContentPermissionsTests var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act - var result = contentPermissions.CheckPermissions(-1, user, out IContent _, new[] { 'B' }); + var result = contentPermissions.CheckPermissions(-1, user, out IContent _, new[] { "B" }.ToHashSet()); // Assert Assert.AreEqual(ContentPermissions.ContentAccess.Denied, result); @@ -291,7 +291,7 @@ public class ContentPermissionsTests var user = CreateUser(); var userServiceMock = new Mock(); - var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A" }) }; + var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A" }.ToHashSet()) }; var permissionSet = new EntityPermissionSet(-20, permissions); userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-20")).Returns(permissionSet); @@ -303,7 +303,7 @@ public class ContentPermissionsTests var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act - var result = contentPermissions.CheckPermissions(-20, user, out IContent _, new[] { 'A' }); + var result = contentPermissions.CheckPermissions(-20, user, out IContent _, new[] { "A" }.ToHashSet()); // Assert Assert.AreEqual(ContentPermissions.ContentAccess.Granted, result); @@ -316,7 +316,7 @@ public class ContentPermissionsTests var user = CreateUser(withUserGroup: false); var userServiceMock = new Mock(); - var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A" }) }; + var permissions = new EntityPermissionCollection { new(9876, 1234, new[] { "A" }.ToHashSet()) }; var permissionSet = new EntityPermissionSet(1234, permissions); userServiceMock.Setup(x => x.GetPermissionsForPath(user, "-20")).Returns(permissionSet); var userService = userServiceMock.Object; @@ -327,7 +327,7 @@ public class ContentPermissionsTests var contentPermissions = new ContentPermissions(userService, contentService, entityService, AppCaches.Disabled); // Act - var result = contentPermissions.CheckPermissions(-20, user, out IContent _, new[] { 'B' }); + var result = contentPermissions.CheckPermissions(-20, user, out IContent _, new[] { "B" }.ToHashSet()); // Assert Assert.AreEqual(ContentPermissions.ContentAccess.Denied, result); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs index d040afb137..f50a81418e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Editors; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -116,7 +117,7 @@ public class UserEditorAuthorizationHelperTests { var currentUser = Mock.Of(user => user.Groups == new[] { - new ReadOnlyUserGroup(1, Guid.NewGuid(), "CurrentUser", "icon-user", null, null, groupAlias, new int[0], new string[0], new string[0], new HashSet(), true), + new ReadOnlyUserGroup(1, Guid.NewGuid(), "CurrentUser", "icon-user", null, null, groupAlias, new int[0], new string[0], new HashSet(), new HashSet(), true), }); IUser savingUser = null; // This means it is a new created user diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/UserGroupBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/UserGroupBuilderTests.cs index 8f6a95c3a6..181ad80ee8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/UserGroupBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/UserGroupBuilderTests.cs @@ -20,7 +20,7 @@ public class UserGroupBuilderTests const string testName = "Test"; const int testUserCount = 11; const string testIcon = "icon"; - const string testPermissions = "abc"; + ISet testPermissions = "abc".Select(x=>x.ToString()).ToHashSet(); const int testStartContentId = 3; const int testStartMediaId = 8; @@ -44,7 +44,7 @@ public class UserGroupBuilderTests Assert.AreEqual(testName, userGroup.Name); Assert.AreEqual(testUserCount, userGroup.UserCount); Assert.AreEqual(testIcon, userGroup.Icon); - Assert.AreEqual(testPermissions.Length, userGroup.Permissions.Count()); + Assert.AreEqual(testPermissions.Count, userGroup.Permissions.Count()); Assert.AreEqual(testStartContentId, userGroup.StartContentId); Assert.AreEqual(testStartMediaId, userGroup.StartMediaId); }