diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/ElementControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/ElementControllerBase.cs index 1ca18f073baf..cdae9f8a3891 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/ElementControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/ElementControllerBase.cs @@ -9,8 +9,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Element; [VersionedApiBackOfficeRoute(Constants.UdiEntityType.Element)] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Element))] -// TODO ELEMENTS: backoffice authorization policies -[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)] +[Authorize(Policy = AuthorizationPolicies.TreeAccessElements)] public class ElementControllerBase : ContentControllerBase { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/Folder/ElementFolderControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/Folder/ElementFolderControllerBase.cs index 4051924c6d29..2fca34f0d425 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/Folder/ElementFolderControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/Folder/ElementFolderControllerBase.cs @@ -1,15 +1,17 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Element.Folder; [VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Element}/folder")] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Element))] -// TODO ELEMENTS: backoffice authorization policies +[Authorize(Policy = AuthorizationPolicies.TreeAccessElements)] public abstract class ElementFolderControllerBase : FolderManagementControllerBase { protected ElementFolderControllerBase( diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/MoveElementController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/MoveElementController.cs index 02f0043b15fa..b3b4aa28a938 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/MoveElementController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/MoveElementController.cs @@ -1,9 +1,8 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.ViewModels.Element; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/PublishElementController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/PublishElementController.cs index bb6449459f7c..6176f5eeb18c 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/PublishElementController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/PublishElementController.cs @@ -1,4 +1,4 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/RecycleBin/ElementRecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/RecycleBin/ElementRecycleBinControllerBase.cs index facd2b12a2b6..4d91bc68f620 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/RecycleBin/ElementRecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/RecycleBin/ElementRecycleBinControllerBase.cs @@ -16,11 +16,9 @@ namespace Umbraco.Cms.Api.Management.Controllers.Element.RecycleBin; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.RecycleBin}/{Constants.UdiEntityType.Element}")] -// TODO ELEMENTS: backoffice authorization policies -[RequireDocumentTreeRootAccess] +[RequireElementTreeRootAccess] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Element))] -// TODO ELEMENTS: backoffice authorization policies -[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)] +[Authorize(Policy = AuthorizationPolicies.TreeAccessElements)] public class ElementRecycleBinControllerBase : RecycleBinControllerBase { private readonly IEntityService _entityService; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/AncestorsElementTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/AncestorsElementTreeController.cs index 9583bf0e8bfa..9899644762ab 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/AncestorsElementTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/AncestorsElementTreeController.cs @@ -2,8 +2,11 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Flags; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree; @@ -11,8 +14,15 @@ namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree; [ApiVersion("1.0")] public class AncestorsElementTreeController : ElementTreeControllerBase { - public AncestorsElementTreeController(IEntityService entityService, IUmbracoMapper umbracoMapper, IElementPresentationFactory elementPresentationFactory) - : base(entityService, umbracoMapper, elementPresentationFactory) + public AncestorsElementTreeController( + IEntityService entityService, + FlagProviderCollection flagProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IElementPresentationFactory elementPresentationFactory) + : base(entityService, flagProviders, userStartNodeEntitiesService, dataTypeService, appCaches, backOfficeSecurityAccessor, elementPresentationFactory) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ChildrenElementTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ChildrenElementTreeController.cs index c93fdd187d5b..cc37255bdc36 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ChildrenElementTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ChildrenElementTreeController.cs @@ -3,8 +3,11 @@ using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Flags; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree; @@ -12,8 +15,15 @@ namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree; [ApiVersion("1.0")] public class ChildrenElementTreeController : ElementTreeControllerBase { - public ChildrenElementTreeController(IEntityService entityService, IUmbracoMapper umbracoMapper, IElementPresentationFactory elementPresentationFactory) - : base(entityService, umbracoMapper, elementPresentationFactory) + public ChildrenElementTreeController( + IEntityService entityService, + FlagProviderCollection flagProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IElementPresentationFactory elementPresentationFactory) + : base(entityService, flagProviders, userStartNodeEntitiesService, dataTypeService, appCaches, backOfficeSecurityAccessor, elementPresentationFactory) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ElementTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ElementTreeControllerBase.cs index 6643105cdc12..69493e109adf 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ElementTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/ElementTreeControllerBase.cs @@ -1,29 +1,42 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Routing; -using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Flags; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Authorization; namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Tree}/{Constants.UdiEntityType.Element}")] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Element))] -// TODO ELEMENTS: backoffice authorization policies -public class ElementTreeControllerBase : FolderTreeControllerBase +[Authorize(Policy = AuthorizationPolicies.SectionAccessForElementTree)] +public class ElementTreeControllerBase : UserStartNodeFolderTreeControllerBase { - private readonly IUmbracoMapper _umbracoMapper; + private readonly AppCaches _appCaches; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IElementPresentationFactory _elementPresentationFactory; - public ElementTreeControllerBase(IEntityService entityService, IUmbracoMapper umbracoMapper, IElementPresentationFactory elementPresentationFactory) - : base(entityService) + public ElementTreeControllerBase( + IEntityService entityService, + FlagProviderCollection flagProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IElementPresentationFactory elementPresentationFactory) + : base(entityService, flagProviders, userStartNodeEntitiesService, dataTypeService) { - _umbracoMapper = umbracoMapper; + _appCaches = appCaches; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _elementPresentationFactory = elementPresentationFactory; } @@ -31,18 +44,39 @@ public ElementTreeControllerBase(IEntityService entityService, IUmbracoMapper um protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.ElementContainer; - protected override ElementTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) - => entities.Select(entity => + protected override int[] GetUserStartNodeIds() + => _backOfficeSecurityAccessor + .BackOfficeSecurity? + .CurrentUser? + .CalculateElementStartNodeIds(EntityService, _appCaches) + ?? []; + + protected override string[] GetUserStartNodePaths() + => _backOfficeSecurityAccessor + .BackOfficeSecurity? + .CurrentUser? + .GetElementStartNodePaths(EntityService, _appCaches) + ?? []; + + protected override ElementTreeItemResponseModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + ElementTreeItemResponseModel responseModel = base.MapTreeItemViewModel(parentKey, entity); + + if (entity is IElementEntitySlim elementEntitySlim) { - ElementTreeItemResponseModel responseModel = MapTreeItemViewModel(parentKey, entity); - if (entity is IElementEntitySlim elementEntitySlim) - { - responseModel.HasChildren = false; - responseModel.CreateDate = elementEntitySlim.CreateDate; - responseModel.DocumentType = _elementPresentationFactory.CreateDocumentTypeReferenceResponseModel(elementEntitySlim); - responseModel.Variants = _elementPresentationFactory.CreateVariantsItemResponseModels(elementEntitySlim); - } - - return responseModel; - }).ToArray(); + responseModel.HasChildren = false; + responseModel.CreateDate = elementEntitySlim.CreateDate; + responseModel.DocumentType = _elementPresentationFactory.CreateDocumentTypeReferenceResponseModel(elementEntitySlim); + responseModel.Variants = _elementPresentationFactory.CreateVariantsItemResponseModels(elementEntitySlim); + } + + return responseModel; + } + + protected override ElementTreeItemResponseModel MapTreeItemViewModelAsNoAccess(Guid? parentKey, IEntitySlim entity) + { + ElementTreeItemResponseModel viewModel = MapTreeItemViewModel(parentKey, entity); + viewModel.NoAccess = true; + return viewModel; + } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/RootElementTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/RootElementTreeController.cs index a8c2cf39f50f..792243e02a9e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/RootElementTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/RootElementTreeController.cs @@ -3,8 +3,11 @@ using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Flags; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree; @@ -12,8 +15,15 @@ namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree; [ApiVersion("1.0")] public class RootElementTreeController : ElementTreeControllerBase { - public RootElementTreeController(IEntityService entityService, IUmbracoMapper umbracoMapper, IElementPresentationFactory elementPresentationFactory) - : base(entityService, umbracoMapper, elementPresentationFactory) + public RootElementTreeController( + IEntityService entityService, + FlagProviderCollection flagProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IElementPresentationFactory elementPresentationFactory) + : base(entityService, flagProviders, userStartNodeEntitiesService, dataTypeService, appCaches, backOfficeSecurityAccessor, elementPresentationFactory) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/SiblingsElementTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/SiblingsElementTreeController.cs index 8c2ad239ba9d..70d1e6e5a400 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/SiblingsElementTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/Tree/SiblingsElementTreeController.cs @@ -1,21 +1,34 @@ +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Flags; using Umbraco.Cms.Api.Management.ViewModels.Tree; -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.Element.Tree; +[ApiVersion("1.0")] public class SiblingsElementTreeController : ElementTreeControllerBase { - public SiblingsElementTreeController(IEntityService entityService, IUmbracoMapper umbracoMapper, IElementPresentationFactory elementPresentationFactory) - : base(entityService, umbracoMapper, elementPresentationFactory) + public SiblingsElementTreeController( + IEntityService entityService, + FlagProviderCollection flagProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IElementPresentationFactory elementPresentationFactory) + : base(entityService, flagProviders, userStartNodeEntitiesService, dataTypeService, appCaches, backOfficeSecurityAccessor, elementPresentationFactory) { } [HttpGet("siblings")] + [MapToApiVersion("1.0")] [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] public async Task>> Siblings( CancellationToken cancellationToken, diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Element/UnpublishElementController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Element/UnpublishElementController.cs index 5104186f4cea..440b50dcfef2 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Element/UnpublishElementController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Element/UnpublishElementController.cs @@ -1,7 +1,6 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.ViewModels.Element; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Security; @@ -15,16 +14,13 @@ public class UnpublishElementController : ElementControllerBase { private readonly IElementPublishingService _elementPublishingService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IDocumentPresentationFactory _documentPresentationFactory; public UnpublishElementController( IElementPublishingService elementPublishingService, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IDocumentPresentationFactory documentPresentationFactory) + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { _elementPublishingService = elementPublishingService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _documentPresentationFactory = documentPresentationFactory; } [HttpPut("{id:guid}/unpublish")] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeFolderTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeFolderTreeControllerBase.cs new file mode 100644 index 000000000000..4075b8ad9713 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeFolderTreeControllerBase.cs @@ -0,0 +1,181 @@ +using Umbraco.Cms.Api.Management.Models.Entities; +using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.Flags; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.Tree; + +/// +/// Base class for folder tree controllers that support user start node filtering. +/// +/// The type of tree item response model. +/// +/// This base class combines folder tree support from +/// with user start node filtering, similar to . +/// Users without root access will only see items within their configured start nodes, +/// with ancestor items marked as "no access" for navigation purposes. +/// +public abstract class UserStartNodeFolderTreeControllerBase : FolderTreeControllerBase + where TItem : FolderTreeItemResponseModel, new() +{ + private readonly IUserStartNodeEntitiesService _userStartNodeEntitiesService; + private readonly IDataTypeService _dataTypeService; + + private Dictionary _accessMap = new(); + private Guid? _dataTypeKey; + + /// + /// Initializes a new instance of the class. + /// + /// The entity service. + /// The flag provider collection. + /// The user start node entities service. + /// The data type service. + protected UserStartNodeFolderTreeControllerBase( + IEntityService entityService, + FlagProviderCollection flagProviders, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService) + : base(entityService, flagProviders) + { + _userStartNodeEntitiesService = userStartNodeEntitiesService; + _dataTypeService = dataTypeService; + } + + private UmbracoObjectTypes[] TreeObjectTypes => [FolderObjectType, ItemObjectType]; + + /// + /// Gets the calculated start node IDs for the current user. + /// + /// An array of start node IDs. + protected abstract int[] GetUserStartNodeIds(); + + /// + /// Gets the calculated start node paths for the current user. + /// + /// An array of start node paths. + protected abstract string[] GetUserStartNodePaths(); + + /// + /// Configures the controller to ignore user start nodes for a specific data type. + /// + /// The data type key, or null to disable. + protected void IgnoreUserStartNodesForDataType(Guid? dataTypeKey) => _dataTypeKey = dataTypeKey; + + /// + protected override IEntitySlim[] GetPagedRootEntities(int skip, int take, out long totalItems) + => UserHasRootAccess() || IgnoreUserStartNodes() + ? base.GetPagedRootEntities(skip, take, out totalItems) + : CalculateAccessMap(() => _userStartNodeEntitiesService.RootUserAccessEntities(TreeObjectTypes, UserStartNodeIds), out totalItems); + + /// + protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip, int take, out long totalItems) + { + if (UserHasRootAccess() || IgnoreUserStartNodes()) + { + return base.GetPagedChildEntities(parentKey, skip, take, out totalItems); + } + + IEnumerable userAccessEntities = _userStartNodeEntitiesService.ChildUserAccessEntities( + TreeObjectTypes, + UserStartNodePaths, + parentKey, + skip, + take, + ItemOrdering, + out totalItems); + + return CalculateAccessMap(() => userAccessEntities, out _); + } + + /// + protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter) + { + if (UserHasRootAccess() || IgnoreUserStartNodes()) + { + return base.GetSiblingEntities(target, before, after, out totalBefore, out totalAfter); + } + + IEnumerable userAccessEntities = _userStartNodeEntitiesService.SiblingUserAccessEntities( + TreeObjectTypes, + UserStartNodePaths, + target, + before, + after, + ItemOrdering, + out totalBefore, + out totalAfter); + + return CalculateAccessMap(() => userAccessEntities, out _); + } + + /// + protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + { + if (UserHasRootAccess() || IgnoreUserStartNodes()) + { + return base.MapTreeItemViewModels(parentKey, entities); + } + + // for users with no root access, only add items for the entities contained within the calculated access map. + // the access map may contain entities that the user does not have direct access to, but need still to see, + // because it has descendants that the user *does* have access to. these entities are added as "no access" items. + TItem[] treeItemViewModels = entities.Select(entity => + { + if (_accessMap.TryGetValue(entity.Key, out var hasAccess) is false) + { + // entity is not a part of the calculated access map + return null; + } + + // direct access => return a regular item + // no direct access => return a "no access" item + return hasAccess + ? MapTreeItemViewModel(parentKey, entity) + : MapTreeItemViewModelAsNoAccess(parentKey, entity); + }) + .WhereNotNull() + .ToArray(); + + return treeItemViewModels; + } + + /// + /// Maps an entity to a tree item view model marked as "no access". + /// + /// The parent key. + /// The entity to map. + /// The mapped tree item view model. + /// + /// Subclasses should override this to set the appropriate "no access" flag on the view model. + /// + protected virtual TItem MapTreeItemViewModelAsNoAccess(Guid? parentKey, IEntitySlim entity) + => MapTreeItemViewModel(parentKey, entity); + + private int[] UserStartNodeIds => field ??= GetUserStartNodeIds(); + + private string[] UserStartNodePaths => field ??= GetUserStartNodePaths(); + + private bool UserHasRootAccess() => UserStartNodeIds.Contains(Constants.System.Root); + + private bool IgnoreUserStartNodes() + => _dataTypeKey.HasValue + && _dataTypeService.IsDataTypeIgnoringUserStartNodes(_dataTypeKey.Value); + + private IEntitySlim[] CalculateAccessMap(Func> getUserAccessEntities, out long totalItems) + { + UserAccessEntity[] userAccessEntities = getUserAccessEntities().ToArray(); + + _accessMap = userAccessEntities.ToDictionary(uae => uae.Entity.Key, uae => uae.HasAccess); + + IEntitySlim[] entities = userAccessEntities.Select(uae => uae.Entity).ToArray(); + totalItems = entities.Length; + + return entities; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupControllerBase.cs index 0112df812797..e717bb93d397 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupControllerBase.cs @@ -53,6 +53,10 @@ protected IActionResult UserGroupOperationStatusResult(UserGroupOperationStatus .WithTitle("Media start node key not found") .WithDetail("The assigned media start node does not exists.") .Build()), + UserGroupOperationStatus.ElementStartNodeKeyNotFound => NotFound(problemDetailsBuilder + .WithTitle("Element start node key not found") + .WithDetail("The assigned element start node does not exist.") + .Build()), UserGroupOperationStatus.DocumentPermissionKeyNotFound => NotFound(new ProblemDetailsBuilder() .WithTitle("Document permission key not found") .WithDetail("An assigned document permission does not reference an existing document.") diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs index 842c7f39358d..c9c012acff35 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/BackOfficeAuthPolicyBuilderExtensions.cs @@ -79,16 +79,27 @@ void AddAllowedApplicationsPolicy(string policyName, params string[] allowedClai Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members); + AddAllowedApplicationsPolicy( + AuthorizationPolicies.SectionAccessForElementTree, + Constants.Applications.Content, + Constants.Applications.Media, + Constants.Applications.Users, + Constants.Applications.Settings, + Constants.Applications.Packages, + Constants.Applications.Members, + Constants.Applications.Library); AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMedia, Constants.Applications.Media); AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMembers, Constants.Applications.Members); AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessPackages, Constants.Applications.Packages); AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessSettings, Constants.Applications.Settings); AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessUsers, Constants.Applications.Users); + AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessLibrary, Constants.Applications.Library); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDataTypes, Constants.Applications.Settings); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDictionary, Constants.Applications.Translation); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDictionaryOrTemplates, Constants.Applications.Translation, Constants.Applications.Settings); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocuments, Constants.Applications.Content); + AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessElements, Constants.Applications.Library); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, Constants.Applications.Content, Constants.Applications.Settings); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentOrMediaOrContentTypes, Constants.Applications.Content, Constants.Applications.Settings, Constants.Applications.Media); AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentsOrMediaOrMembersOrContentTypes, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings); diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs index e4fea6347097..80bd517b793a 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs @@ -42,6 +42,8 @@ public async Task CreateAsync(IUserGroup userGroup) var contentRootAccess = contentStartNodeKey is null && userGroup.StartContentId == Constants.System.Root; Guid? mediaStartNodeKey = GetKeyFromId(userGroup.StartMediaId, UmbracoObjectTypes.Media); var mediaRootAccess = mediaStartNodeKey is null && userGroup.StartMediaId == Constants.System.Root; + Guid? elementStartNodeKey = GetKeyFromId(userGroup.StartElementId, UmbracoObjectTypes.ElementContainer); + var elementRootAccess = elementStartNodeKey is null && userGroup.StartElementId == Constants.System.Root; Attempt, UserGroupOperationStatus> languageIsoCodesMappingAttempt = await MapLanguageIdsToIsoCodeAsync(userGroup.AllowedLanguages); @@ -60,6 +62,8 @@ public async Task CreateAsync(IUserGroup userGroup) DocumentRootAccess = contentRootAccess, MediaStartNode = ReferenceByIdModel.ReferenceOrNull(mediaStartNodeKey), MediaRootAccess = mediaRootAccess, + ElementStartNode = ReferenceByIdModel.ReferenceOrNull(elementStartNodeKey), + ElementRootAccess = elementRootAccess, Icon = userGroup.Icon, Languages = languageIsoCodesMappingAttempt.Result, HasAccessToAllLanguages = userGroup.HasAccessToAllLanguages, @@ -77,6 +81,7 @@ public async Task CreateAsync(IReadOnlyUserGroup userGro // TODO figure out how to reuse code from Task CreateAsync(IUserGroup userGroup) instead of copying Guid? contentStartNodeKey = GetKeyFromId(userGroup.StartContentId, UmbracoObjectTypes.Document); Guid? mediaStartNodeKey = GetKeyFromId(userGroup.StartMediaId, UmbracoObjectTypes.Media); + Guid? elementStartNodeKey = GetKeyFromId(userGroup.StartElementId, UmbracoObjectTypes.ElementContainer); Attempt, UserGroupOperationStatus> languageIsoCodesMappingAttempt = await MapLanguageIdsToIsoCodeAsync(userGroup.AllowedLanguages); if (languageIsoCodesMappingAttempt.Success is false) @@ -92,6 +97,7 @@ public async Task CreateAsync(IReadOnlyUserGroup userGro Alias = userGroup.Alias, DocumentStartNode = ReferenceByIdModel.ReferenceOrNull(contentStartNodeKey), MediaStartNode = ReferenceByIdModel.ReferenceOrNull(mediaStartNodeKey), + ElementStartNode = ReferenceByIdModel.ReferenceOrNull(elementStartNodeKey), Icon = userGroup.Icon, Languages = languageIsoCodesMappingAttempt.Result, HasAccessToAllLanguages = userGroup.HasAccessToAllLanguages, @@ -203,7 +209,7 @@ public async Task> UpdateAsync(IUs current.Description = request.Description; current.Icon = request.Icon; current.HasAccessToAllLanguages = request.HasAccessToAllLanguages; - + current.Permissions = request.FallbackPermissions; current.GranularPermissions = await _permissionPresentationFactory.CreatePermissionSetsAsync(request.Permissions); @@ -278,6 +284,26 @@ private Attempt AssignStartNodesToUserGroup(UserGroupB target.StartMediaId = null; } + if (source.ElementStartNode is not null) + { + var elementId = GetIdFromKey(source.ElementStartNode.Id, UmbracoObjectTypes.ElementContainer); + + if (elementId is null) + { + return Attempt.Fail(UserGroupOperationStatus.ElementStartNodeKeyNotFound); + } + + target.StartElementId = elementId; + } + else if (source.ElementRootAccess) + { + target.StartElementId = Constants.System.Root; + } + else + { + target.StartElementId = null; + } + return Attempt.Succeed(UserGroupOperationStatus.Success); } diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs index bf5e085cfa51..651506b38d8f 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs @@ -89,6 +89,8 @@ public UserResponseModel CreateResponseModel(IUser user) HasDocumentRootAccess = HasRootAccess(user.StartContentIds), MediaStartNodeIds = GetKeysFromIds(user.StartMediaIds, UmbracoObjectTypes.Media), HasMediaRootAccess = HasRootAccess(user.StartMediaIds), + ElementStartNodeIds = GetKeysFromIds(user.StartElementIds, UmbracoObjectTypes.ElementContainer), + HasElementRootAccess = HasRootAccess(user.StartElementIds), FailedLoginAttempts = user.FailedPasswordAttempts, LastLoginDate = user.LastLoginDate, LastLockoutDate = user.LastLockoutDate, @@ -198,6 +200,8 @@ public Task CreateUpdateModelAsync(Guid existingUserKey, Update HasContentRootAccess = updateModel.HasDocumentRootAccess, MediaStartNodeKeys = updateModel.MediaStartNodeIds.Select(x => x.Id).ToHashSet(), HasMediaRootAccess = updateModel.HasMediaRootAccess, + ElementStartNodeKeys = updateModel.ElementStartNodeIds.Select(x => x.Id).ToHashSet(), + HasElementRootAccess = updateModel.HasElementRootAccess, UserGroupKeys = updateModel.UserGroupIds.Select(x => x.Id).ToHashSet() }; @@ -214,6 +218,8 @@ public async Task CreateCurrentUserResponseModelAsync( ISet mediaStartNodeKeys = GetKeysFromIds(mediaStartNodeIds, UmbracoObjectTypes.Media); var contentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches); ISet documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document); + var elementStartNodeIds = user.CalculateElementStartNodeIds(_entityService, _appCaches); + ISet elementStartNodeKeys = GetKeysFromIds(elementStartNodeIds, UmbracoObjectTypes.ElementContainer); HashSet permissions = GetAggregatedGranularPermissions(user, presentationGroups); var fallbackPermissions = presentationGroups.SelectMany(x => x.FallbackPermissions).ToHashSet(); @@ -235,6 +241,8 @@ public async Task CreateCurrentUserResponseModelAsync( HasMediaRootAccess = HasRootAccess(mediaStartNodeIds), DocumentStartNodeIds = documentStartNodeKeys, HasDocumentRootAccess = HasRootAccess(contentStartNodeIds), + ElementStartNodeIds = elementStartNodeKeys, + HasElementRootAccess = HasRootAccess(elementStartNodeIds), Permissions = permissions, FallbackPermissions = fallbackPermissions, HasAccessToAllLanguages = hasAccessToAllLanguages, @@ -303,6 +311,8 @@ public Task CreateCalculatedUserStartNode ISet mediaStartNodeKeys = GetKeysFromIds(mediaStartNodeIds, UmbracoObjectTypes.Media); var contentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches); ISet documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document); + var elementStartNodeIds = user.CalculateElementStartNodeIds(_entityService, _appCaches); + ISet elementStartNodeKeys = GetKeysFromIds(elementStartNodeIds, UmbracoObjectTypes.ElementContainer); return Task.FromResult(new CalculatedUserStartNodesResponseModel() { @@ -311,6 +321,8 @@ public Task CreateCalculatedUserStartNode HasMediaRootAccess = HasRootAccess(mediaStartNodeIds), DocumentStartNodeIds = documentStartNodeKeys, HasDocumentRootAccess = HasRootAccess(contentStartNodeIds), + ElementStartNodeIds = elementStartNodeKeys, + HasElementRootAccess = HasRootAccess(elementStartNodeIds), }); } diff --git a/src/Umbraco.Cms.Api.Management/Filters/RequireElementTreeRootAccessAttribute.cs b/src/Umbraco.Cms.Api.Management/Filters/RequireElementTreeRootAccessAttribute.cs new file mode 100644 index 000000000000..92a9b03e60d9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Filters/RequireElementTreeRootAccessAttribute.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Filters; + +public class RequireElementTreeRootAccessAttribute : RequireTreeRootAccessAttribute +{ + protected override int[] GetUserStartNodeIds(IUser user, ActionExecutingContext context) + { + AppCaches appCaches = context.HttpContext.RequestServices.GetRequiredService(); + IEntityService entityService = context.HttpContext.RequestServices.GetRequiredService(); + + return user.CalculateElementStartNodeIds(entityService, appCaches) ?? Array.Empty(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Cms.Api.Management/Mapping/SectionMapper.cs b/src/Umbraco.Cms.Api.Management/Mapping/SectionMapper.cs index d125fcd73571..adb97ab5775c 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/SectionMapper.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/SectionMapper.cs @@ -17,6 +17,7 @@ public static class SectionMapper new SectionMapping { Alias = "translation", Name = "Umb.Section.Translation" }, new SectionMapping { Alias = "users", Name = "Umb.Section.Users" }, new SectionMapping { Alias = "forms", Name = "Umb.Section.Forms" }, + new SectionMapping { Alias = "library", Name = "Umb.Section.Library" }, }; public static string GetName(string alias) diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 7e8a08be293d..bbb924d66e6c 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -53,7 +53,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -199,7 +199,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -259,7 +259,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -368,7 +368,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -508,7 +508,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -639,7 +639,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -695,7 +695,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -811,7 +811,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -875,7 +875,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -910,7 +910,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1056,7 +1056,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1116,7 +1116,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -1225,7 +1225,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -1365,7 +1365,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1442,7 +1442,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1491,7 +1491,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1550,7 +1550,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1598,7 +1598,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1669,7 +1669,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1732,7 +1732,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1801,7 +1801,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -1863,7 +1863,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -2033,7 +2033,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2093,7 +2093,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -2202,7 +2202,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -2342,7 +2342,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2411,7 +2411,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2553,7 +2553,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2699,7 +2699,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2748,7 +2748,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2796,7 +2796,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2859,7 +2859,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -2914,7 +2914,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3060,7 +3060,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3120,7 +3120,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -3229,7 +3229,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -3369,7 +3369,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3485,7 +3485,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3545,7 +3545,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3691,7 +3691,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -3751,7 +3751,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -3860,7 +3860,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -4000,7 +4000,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4120,7 +4120,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4169,7 +4169,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4217,7 +4217,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4288,7 +4288,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4351,7 +4351,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4420,7 +4420,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4566,7 +4566,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4626,7 +4626,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -4709,7 +4709,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -4849,7 +4849,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -4935,7 +4935,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5013,7 +5013,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5090,7 +5090,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5247,7 +5247,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5308,7 +5308,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5450,7 +5450,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5592,7 +5592,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5723,7 +5723,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5778,7 +5778,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5871,7 +5871,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -5906,7 +5906,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6052,7 +6052,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6112,7 +6112,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -6221,7 +6221,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -6361,7 +6361,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6507,7 +6507,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6556,7 +6556,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6622,7 +6622,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6670,7 +6670,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6741,7 +6741,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6804,7 +6804,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6873,7 +6873,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -6972,7 +6972,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7046,7 +7046,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7164,7 +7164,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7282,7 +7282,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7411,7 +7411,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7557,7 +7557,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7617,7 +7617,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -7726,7 +7726,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -7866,7 +7866,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -7945,7 +7945,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8024,7 +8024,7 @@ "deprecated": true, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8155,7 +8155,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8215,7 +8215,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -8381,7 +8381,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8497,7 +8497,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8608,7 +8608,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8671,7 +8671,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -8785,7 +8785,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -8880,7 +8880,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9011,7 +9011,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -9094,7 +9094,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -9152,7 +9152,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -9266,7 +9266,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9408,7 +9408,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9561,7 +9561,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9644,7 +9644,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9704,7 +9704,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9782,7 +9782,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -9860,7 +9860,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10002,7 +10002,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10144,7 +10144,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10211,7 +10211,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10246,7 +10246,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10377,7 +10377,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10429,7 +10429,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10560,7 +10560,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10609,7 +10609,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10709,7 +10709,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10783,7 +10783,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10894,7 +10894,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -10968,7 +10968,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11110,7 +11110,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11173,7 +11173,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11228,7 +11228,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11283,7 +11283,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11352,7 +11352,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11400,7 +11400,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11471,7 +11471,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11534,7 +11534,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11603,7 +11603,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11693,7 +11693,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11727,7 +11727,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11873,7 +11873,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -11933,7 +11933,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -12042,7 +12042,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -12182,7 +12182,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12313,7 +12313,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12429,7 +12429,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12540,7 +12540,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12682,7 +12682,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12824,7 +12824,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -12966,7 +12966,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13128,11 +13128,26 @@ }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } } }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13185,11 +13200,14 @@ }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource" } }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -13279,11 +13297,26 @@ }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } } }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -13404,11 +13437,26 @@ }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } } }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13531,11 +13579,26 @@ }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } } }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13627,11 +13690,26 @@ }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } } }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13762,7 +13840,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13807,11 +13885,14 @@ }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource" } }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13885,7 +13966,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -13996,7 +14077,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14059,7 +14140,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14170,7 +14251,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14225,7 +14306,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14294,7 +14375,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14335,11 +14416,14 @@ }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource" } }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14403,11 +14487,14 @@ }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource" } }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14463,11 +14550,14 @@ }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource" } }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14529,11 +14619,14 @@ }, "401": { "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource" } }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14588,7 +14681,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14647,7 +14740,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14742,7 +14835,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14858,7 +14951,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -14947,7 +15040,7 @@ "deprecated": true, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15024,7 +15117,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15094,7 +15187,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15146,7 +15239,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15202,7 +15295,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15323,7 +15416,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15573,7 +15666,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15605,7 +15698,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15657,7 +15750,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -15801,7 +15894,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -15857,7 +15950,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -15965,7 +16058,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -16104,7 +16197,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16159,7 +16252,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16226,7 +16319,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16321,7 +16414,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16406,7 +16499,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16461,7 +16554,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -16579,7 +16672,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16638,7 +16731,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -16720,7 +16813,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16776,7 +16869,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16814,7 +16907,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16852,7 +16945,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16928,7 +17021,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -16987,7 +17080,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17039,7 +17132,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17098,7 +17191,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17244,7 +17337,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17304,7 +17397,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -17387,7 +17480,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -17527,7 +17620,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17613,7 +17706,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17690,7 +17783,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17847,7 +17940,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -17908,7 +18001,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18050,7 +18143,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18192,7 +18285,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18247,7 +18340,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18340,7 +18433,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18375,7 +18468,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18521,7 +18614,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18581,7 +18674,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -18690,7 +18783,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -18830,7 +18923,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -18976,7 +19069,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19024,7 +19117,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19095,7 +19188,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19158,7 +19251,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19227,7 +19320,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19348,7 +19441,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19397,7 +19490,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19497,7 +19590,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19643,7 +19736,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -19703,7 +19796,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -19812,7 +19905,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -19952,7 +20045,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20031,7 +20124,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20147,7 +20240,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20258,7 +20351,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20336,7 +20429,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20414,7 +20507,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20556,7 +20649,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20623,7 +20716,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20659,7 +20752,7 @@ "deprecated": true, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20790,7 +20883,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20842,7 +20935,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -20973,7 +21066,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21047,7 +21140,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21158,7 +21251,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21232,7 +21325,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21374,7 +21467,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21437,7 +21530,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21492,7 +21585,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21547,7 +21640,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21616,7 +21709,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21664,7 +21757,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21735,7 +21828,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21798,7 +21891,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21867,7 +21960,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21916,7 +22009,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -21971,7 +22064,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -22089,7 +22182,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22138,7 +22231,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -22247,7 +22340,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -22387,7 +22480,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22442,7 +22535,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22491,7 +22584,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22550,7 +22643,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22696,7 +22789,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -22756,7 +22849,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -22839,7 +22932,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -22979,7 +23072,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23056,7 +23149,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23213,7 +23306,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23274,7 +23367,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23416,7 +23509,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23558,7 +23651,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23651,7 +23744,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23686,7 +23779,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23832,7 +23925,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -23892,7 +23985,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -24001,7 +24094,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -24141,7 +24234,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24287,7 +24380,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24335,7 +24428,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24406,7 +24499,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24469,7 +24562,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24538,7 +24631,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24658,7 +24751,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24707,7 +24800,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24777,7 +24870,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24923,7 +25016,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -24983,7 +25076,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -25092,7 +25185,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -25232,7 +25325,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25310,7 +25403,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25388,7 +25481,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25530,7 +25623,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25597,7 +25690,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25632,7 +25725,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25763,7 +25856,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25837,7 +25930,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25872,7 +25965,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25907,7 +26000,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25939,7 +26032,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -25991,7 +26084,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26052,7 +26145,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26162,7 +26255,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26197,7 +26290,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26252,7 +26345,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -26396,7 +26489,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26456,7 +26549,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -26539,7 +26632,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -26653,7 +26746,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26714,7 +26807,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26769,7 +26862,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26817,7 +26910,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -26963,7 +27056,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27022,7 +27115,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -27130,7 +27223,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -27269,7 +27362,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27425,7 +27518,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27571,7 +27664,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27630,7 +27723,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -27738,7 +27831,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27793,7 +27886,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27852,7 +27945,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27899,7 +27992,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -27961,7 +28054,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28016,7 +28109,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28076,7 +28169,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28133,7 +28226,7 @@ "deprecated": true, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28168,7 +28261,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -28245,7 +28338,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28307,7 +28400,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28340,7 +28433,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28372,7 +28465,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28405,7 +28498,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28481,7 +28574,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28545,7 +28638,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -28602,7 +28695,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28637,7 +28730,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -28692,7 +28785,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28741,7 +28834,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28796,7 +28889,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28856,7 +28949,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28934,7 +29027,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -28982,7 +29075,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29128,7 +29221,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29187,7 +29280,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -29295,7 +29388,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -29434,7 +29527,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29590,7 +29683,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29736,7 +29829,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29795,7 +29888,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -29903,7 +29996,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -29950,7 +30043,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30012,7 +30105,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30067,7 +30160,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30127,7 +30220,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30179,7 +30272,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30260,7 +30353,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30295,7 +30388,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30400,7 +30493,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30531,7 +30624,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30716,7 +30809,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30772,7 +30865,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30842,7 +30935,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30878,7 +30971,7 @@ "deprecated": true, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30926,7 +31019,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -30970,7 +31063,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31029,7 +31122,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31081,7 +31174,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31129,7 +31222,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31275,7 +31368,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31334,7 +31427,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -31442,7 +31535,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -31581,7 +31674,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31737,7 +31830,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31883,7 +31976,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -31942,7 +32035,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -32050,7 +32143,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32097,7 +32190,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32159,7 +32252,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32214,7 +32307,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32274,7 +32367,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32347,7 +32440,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32402,7 +32495,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32437,7 +32530,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -32540,7 +32633,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32589,7 +32682,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32648,7 +32741,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32794,7 +32887,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -32854,7 +32947,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -32963,7 +33056,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -33103,7 +33196,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33138,7 +33231,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33228,7 +33321,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33263,7 +33356,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33311,7 +33404,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33374,7 +33467,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33429,7 +33522,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33490,7 +33583,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33595,7 +33688,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33666,7 +33759,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -33760,7 +33853,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33792,7 +33885,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33892,7 +33985,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -33941,7 +34034,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34064,7 +34157,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -34134,7 +34227,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -34240,7 +34333,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34286,7 +34379,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -34372,7 +34465,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34463,7 +34556,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34512,7 +34605,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34617,7 +34710,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -34735,7 +34828,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -34788,7 +34881,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -34848,7 +34941,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -34931,7 +35024,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -35045,7 +35138,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35170,7 +35263,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -35293,7 +35386,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35421,7 +35514,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35470,7 +35563,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35616,7 +35709,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -35719,7 +35812,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -35786,7 +35879,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -35846,7 +35939,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -35955,7 +36048,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -36095,7 +36188,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36158,7 +36251,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36277,7 +36370,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36337,7 +36430,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36479,7 +36572,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36595,7 +36688,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -36638,7 +36731,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36731,7 +36824,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36853,7 +36946,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -36964,7 +37057,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -37104,7 +37197,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37139,7 +37232,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37171,7 +37264,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37206,7 +37299,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37308,7 +37401,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -37443,7 +37536,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -37511,7 +37604,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37601,7 +37694,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37691,7 +37784,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37726,7 +37819,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37761,7 +37854,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37821,7 +37914,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37884,7 +37977,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -37944,7 +38037,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38075,7 +38168,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38206,7 +38299,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38352,7 +38445,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38606,7 +38699,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38819,7 +38912,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38924,7 +39017,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -38973,7 +39066,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39028,7 +39121,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -39172,7 +39265,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39232,7 +39325,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -39341,7 +39434,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] }, @@ -39481,7 +39574,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39545,7 +39638,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39600,7 +39693,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39655,7 +39748,7 @@ }, "security": [ { - "Backoffice-User": [] + "Backoffice-User": [ ] } ] } @@ -39874,7 +39967,9 @@ "CalculatedUserStartNodesResponseModel": { "required": [ "documentStartNodeIds", + "elementStartNodeIds", "hasDocumentRootAccess", + "hasElementRootAccess", "hasMediaRootAccess", "id", "mediaStartNodeIds" @@ -39912,6 +40007,20 @@ }, "hasMediaRootAccess": { "type": "boolean" + }, + "elementStartNodeIds": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "hasElementRootAccess": { + "type": "boolean" } }, "additionalProperties": false @@ -41555,6 +41664,7 @@ "required": [ "alias", "documentRootAccess", + "elementRootAccess", "fallbackPermissions", "hasAccessToAllLanguages", "languages", @@ -41571,6 +41681,10 @@ "alias": { "type": "string" }, + "description": { + "type": "string", + "nullable": true + }, "icon": { "type": "string", "nullable": true @@ -41612,6 +41726,17 @@ "mediaRootAccess": { "type": "boolean" }, + "elementStartNode": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ], + "nullable": true + }, + "elementRootAccess": { + "type": "boolean" + }, "fallbackPermissions": { "uniqueItems": true, "type": "array", @@ -41806,11 +41931,13 @@ "allowedSections", "avatarUrls", "documentStartNodeIds", + "elementStartNodeIds", "email", "fallbackPermissions", "hasAccessToAllLanguages", "hasAccessToSensitiveData", "hasDocumentRootAccess", + "hasElementRootAccess", "hasMediaRootAccess", "id", "isAdmin", @@ -41880,6 +42007,20 @@ "hasMediaRootAccess": { "type": "boolean" }, + "elementStartNodeIds": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "hasElementRootAccess": { + "type": "boolean" + }, "avatarUrls": { "type": "array", "items": { @@ -44415,6 +44556,7 @@ "id", "isFolder", "name", + "noAccess", "variants" ], "type": "object", @@ -44450,6 +44592,9 @@ "isFolder": { "type": "boolean" }, + "noAccess": { + "type": "boolean" + }, "createDate": { "type": "string", "format": "date-time" @@ -44822,7 +44967,7 @@ }, "actionParameters": { "type": "object", - "additionalProperties": {}, + "additionalProperties": { }, "nullable": true } }, @@ -45463,7 +45608,7 @@ }, "extensions": { "type": "array", - "items": {} + "items": { } } }, "additionalProperties": false @@ -49429,7 +49574,7 @@ "nullable": true } }, - "additionalProperties": {} + "additionalProperties": { } }, "ProblemDetailsBuilderModel": { "type": "object", @@ -52640,6 +52785,7 @@ "required": [ "alias", "documentRootAccess", + "elementRootAccess", "fallbackPermissions", "hasAccessToAllLanguages", "languages", @@ -52656,6 +52802,10 @@ "alias": { "type": "string" }, + "description": { + "type": "string", + "nullable": true + }, "icon": { "type": "string", "nullable": true @@ -52697,6 +52847,17 @@ "mediaRootAccess": { "type": "boolean" }, + "elementStartNode": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ], + "nullable": true + }, + "elementRootAccess": { + "type": "boolean" + }, "fallbackPermissions": { "uniqueItems": true, "type": "array", @@ -52759,8 +52920,10 @@ "UpdateUserRequestModel": { "required": [ "documentStartNodeIds", + "elementStartNodeIds", "email", "hasDocumentRootAccess", + "hasElementRootAccess", "hasMediaRootAccess", "languageIsoCode", "mediaStartNodeIds", @@ -52820,6 +52983,20 @@ }, "hasMediaRootAccess": { "type": "boolean" + }, + "elementStartNodeIds": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "hasElementRootAccess": { + "type": "boolean" } }, "additionalProperties": false @@ -53075,6 +53252,7 @@ "alias", "aliasCanBeChanged", "documentRootAccess", + "elementRootAccess", "fallbackPermissions", "hasAccessToAllLanguages", "id", @@ -53093,6 +53271,10 @@ "alias": { "type": "string" }, + "description": { + "type": "string", + "nullable": true + }, "icon": { "type": "string", "nullable": true @@ -53134,6 +53316,17 @@ "mediaRootAccess": { "type": "boolean" }, + "elementStartNode": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ], + "nullable": true + }, + "elementRootAccess": { + "type": "boolean" + }, "fallbackPermissions": { "uniqueItems": true, "type": "array", @@ -53306,9 +53499,11 @@ "avatarUrls", "createDate", "documentStartNodeIds", + "elementStartNodeIds", "email", "failedLoginAttempts", "hasDocumentRootAccess", + "hasElementRootAccess", "hasMediaRootAccess", "id", "isAdmin", @@ -53378,6 +53573,20 @@ "hasMediaRootAccess": { "type": "boolean" }, + "elementStartNodeIds": { + "uniqueItems": true, + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, + "hasElementRootAccess": { + "type": "boolean" + }, "avatarUrls": { "type": "array", "items": { @@ -53865,7 +54074,7 @@ "authorizationCode": { "authorizationUrl": "/umbraco/management/api/v1/security/back-office/authorize", "tokenUrl": "/umbraco/management/api/v1/security/back-office/token", - "scopes": {} + "scopes": { } } } } @@ -54029,4 +54238,4 @@ "name": "Webhook" } ] -} +} \ No newline at end of file diff --git a/src/Umbraco.Cms.Api.Management/Services/Entities/IUserStartNodeEntitiesService.cs b/src/Umbraco.Cms.Api.Management/Services/Entities/IUserStartNodeEntitiesService.cs index 58e758f5c2c9..e026dc6b73f0 100644 --- a/src/Umbraco.Cms.Api.Management/Services/Entities/IUserStartNodeEntitiesService.cs +++ b/src/Umbraco.Cms.Api.Management/Services/Entities/IUserStartNodeEntitiesService.cs @@ -23,6 +23,25 @@ public interface IUserStartNodeEntitiesService /// IEnumerable RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds); + /// + /// Calculates the applicable root entities for multiple object types for users without root access. + /// + /// The object types to query. + /// The calculated start node IDs for the user. + /// A list of root entities for the user across all specified object types. + /// + /// The returned entities may include entities that outside of the user start node scope, but are needed to + /// for browsing to the actual user start nodes. These entities will be marked as "no access" entities. + /// + /// This method does not support pagination, because it must load all entities explicitly in order to calculate + /// the correct result, given that user start nodes can be descendants of root nodes. Consumers need to apply + /// pagination to the result if applicable. + /// + IEnumerable RootUserAccessEntities( + UmbracoObjectTypes[] umbracoObjectTypes, + int[] userStartNodeIds) + => throw new NotImplementedException(); + /// /// Calculates the applicable child entities for a given object type for users without root access. /// @@ -51,6 +70,31 @@ IEnumerable ChildUserAccessEntities( return []; } + /// + /// Calculates the applicable child entities for multiple object types for users without root access. + /// + /// The object types to query. + /// The calculated start node paths for the user. + /// The key of the parent. + /// The number of applicable children to skip. + /// The number of applicable children to take. + /// The ordering to apply when fetching and paginating the children. + /// The total number of applicable children available across all object types. + /// A list of child entities applicable for the user across all specified object types. + /// + /// The returned entities may include entities that outside of the user start node scope, but are needed to + /// for browsing to the actual user start nodes. These entities will be marked as "no access" entities. + /// + IEnumerable ChildUserAccessEntities( + UmbracoObjectTypes[] umbracoObjectTypes, + string[] userStartNodePaths, + Guid parentKey, + int skip, + int take, + Ordering ordering, + out long totalItems) + => throw new NotImplementedException(); + /// /// Calculates the applicable child entities from a list of candidate child entities for users without root access. /// @@ -95,6 +139,33 @@ IEnumerable SiblingUserAccessEntities( return []; } + /// + /// Calculates the applicable sibling entities for multiple object types for users without root access. + /// + /// The object types to query. + /// The calculated start node paths for the user. + /// The key of the target. + /// The number of applicable siblings to retrieve before the target. + /// The number of applicable siblings to retrieve after the target. + /// The ordering to apply when fetching and paginating the siblings. + /// Outputs the total number of siblings before the target entity across all object types. + /// Outputs the total number of siblings after the target entity across all object types. + /// A list of sibling entities applicable for the user across all specified object types. + /// + /// The returned entities may include entities that outside of the user start node scope, but are needed to + /// for browsing to the actual user start nodes. These entities will be marked as "no access" entities. + /// + IEnumerable SiblingUserAccessEntities( + UmbracoObjectTypes[] umbracoObjectTypes, + string[] userStartNodePaths, + Guid targetKey, + int before, + int after, + Ordering ordering, + out long totalBefore, + out long totalAfter) + => throw new NotImplementedException(); + /// /// Calculates the access level of a collection of entities for users without root access. /// diff --git a/src/Umbraco.Cms.Api.Management/Services/Entities/UserStartNodeEntitiesService.cs b/src/Umbraco.Cms.Api.Management/Services/Entities/UserStartNodeEntitiesService.cs index 686433c42984..63941c6926d2 100644 --- a/src/Umbraco.Cms.Api.Management/Services/Entities/UserStartNodeEntitiesService.cs +++ b/src/Umbraco.Cms.Api.Management/Services/Entities/UserStartNodeEntitiesService.cs @@ -1,12 +1,10 @@ -using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Models.Entities; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Api.Management.Models.Entities; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Services.Entities; @@ -18,30 +16,45 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService { private readonly IEntityService _entityService; private readonly ICoreScopeProvider _scopeProvider; - private readonly IIdKeyMap _idKeyMap; /// /// Initializes a new instance of the class. /// /// The entity service. /// The core scope provider. - /// The ID to key mapping service. - public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProvider scopeProvider, IIdKeyMap idKeyMap) + public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProvider scopeProvider) { _entityService = entityService; _scopeProvider = scopeProvider; - _idKeyMap = idKeyMap; + } + + /// + /// Initializes a new instance of the class. + /// + /// The entity service. + /// The core scope provider. + /// The ID to key mapping service. + [Obsolete("Use the constructor without IIdKeyMap. Scheduled for removal in Umbraco 19.")] + public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProvider scopeProvider, IIdKeyMap idKeyMap) + : this(entityService, scopeProvider) + { } /// public IEnumerable RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds) + => RootUserAccessEntities([umbracoObjectType], userStartNodeIds); + + /// + public IEnumerable RootUserAccessEntities(UmbracoObjectTypes[] umbracoObjectTypes, int[] userStartNodeIds) { // Root entities for users without root access should include: // - the start nodes that are actual root entities (level == 1) // - the root level ancestors to the rest of the start nodes (required for browsing to the actual start nodes - will be marked as "no access") + + // Collect start entities from all object types IEntitySlim[] userStartEntities = userStartNodeIds.Any() - ? _entityService.GetAll(umbracoObjectType, userStartNodeIds).ToArray() - : Array.Empty(); + ? _entityService.GetAll(umbracoObjectTypes, userStartNodeIds).ToArray() + : []; // Find the start nodes that are at root level (level == 1). IEntitySlim[] allowedTopmostEntities = userStartEntities.Where(entity => entity.Level == 1).ToArray(); @@ -51,9 +64,11 @@ public IEnumerable RootUserAccessEntities(UmbracoObjectTypes u .Select(entity => int.TryParse(entity.Path.Split(Constants.CharArrays.Comma).Skip(1).FirstOrDefault(), out var id) ? id : 0) .Where(id => id > 0) .ToArray(); + + // Get non-allowed topmost entities from all object types IEntitySlim[] nonAllowedTopmostEntities = nonAllowedTopmostEntityIds.Any() - ? _entityService.GetAll(umbracoObjectType, nonAllowedTopmostEntityIds).ToArray() - : Array.Empty(); + ? _entityService.GetAll(umbracoObjectTypes, nonAllowedTopmostEntityIds).ToArray() + : []; return allowedTopmostEntities .Select(entity => new UserAccessEntity(entity, true)) @@ -72,45 +87,66 @@ public IEnumerable ChildUserAccessEntities( int take, Ordering ordering, out long totalItems) - { - Attempt parentIdAttempt = _idKeyMap.GetIdForKey(parentKey, umbracoObjectType); - if (parentIdAttempt.Success is false) - { - totalItems = 0; - return []; - } + => ChildUserAccessEntities([umbracoObjectType], userStartNodePaths, parentKey, skip, take, ordering, out totalItems); - var parentId = parentIdAttempt.Result; - IEntitySlim? parent = _entityService.Get(parentId); + /// + public IEnumerable ChildUserAccessEntities( + UmbracoObjectTypes[] umbracoObjectTypes, + string[] userStartNodePaths, + Guid parentKey, + int skip, + int take, + Ordering ordering, + out long totalItems) + { + IEntitySlim? parent = _entityService.Get(parentKey); if (parent is null) { totalItems = 0; return []; } - IEntitySlim[] children; if (userStartNodePaths.Any(path => $"{parent.Path},".StartsWith($"{path},"))) { // The requested parent is one of the user start nodes (or a descendant of one), all children are by definition allowed. - children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, ordering: ordering).ToArray(); - return ChildUserAccessEntities(children, userStartNodePaths); + IEnumerable allChildren = _entityService.GetPagedChildren( + parentKey, + umbracoObjectTypes, + umbracoObjectTypes, + skip, + take, + false, + out totalItems, + ordering: ordering); + + return ChildUserAccessEntities(allChildren, userStartNodePaths); } // Need to use a List here because the expression tree cannot convert an array when used in Contains. // See ExpressionTests.Sql_In(). - List allowedChildIds = GetAllowedIds(userStartNodePaths, parentId); + List allowedChildIds = GetAllowedIds(userStartNodePaths, parent.Id); - totalItems = allowedChildIds.Count; if (allowedChildIds.Count == 0) { // The requested parent is outside the scope of any user start nodes. + totalItems = 0; return []; } - // Even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children. + // Fetch allowed children from all object types IQuery query = _scopeProvider.CreateQuery().Where(x => allowedChildIds.Contains(x.Id)); - children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, query, ordering).ToArray(); - return ChildUserAccessEntities(children, userStartNodePaths); + IEnumerable allAllowedChildren = _entityService.GetPagedChildren( + parentKey, + umbracoObjectTypes, + umbracoObjectTypes, + skip, + take, + false, + out totalItems, + query, + ordering); + + return ChildUserAccessEntities(allAllowedChildren, userStartNodePaths); } private static List GetAllowedIds(string[] userStartNodePaths, int parentId) @@ -160,17 +196,20 @@ public IEnumerable SiblingUserAccessEntities( Ordering ordering, out long totalBefore, out long totalAfter) - { - Attempt targetIdAttempt = _idKeyMap.GetIdForKey(targetKey, umbracoObjectType); - if (targetIdAttempt.Success is false) - { - totalBefore = 0; - totalAfter = 0; - return []; - } + => SiblingUserAccessEntities([umbracoObjectType], userStartNodePaths, targetKey, before, after, ordering, out totalBefore, out totalAfter); - var targetId = targetIdAttempt.Result; - IEntitySlim? target = _entityService.Get(targetId); + /// + public IEnumerable SiblingUserAccessEntities( + UmbracoObjectTypes[] umbracoObjectTypes, + string[] userStartNodePaths, + Guid targetKey, + int before, + int after, + Ordering ordering, + out long totalBefore, + out long totalAfter) + { + IEntitySlim? target = _entityService.Get(targetKey); if (target is null) { totalBefore = 0; @@ -178,8 +217,6 @@ public IEnumerable SiblingUserAccessEntities( return []; } - IEntitySlim[] siblings; - IEntitySlim? targetParent = _entityService.Get(target.ParentId); if (targetParent is null) // Even if the parent is the root, we still expect to get a value here. { @@ -188,11 +225,12 @@ public IEnumerable SiblingUserAccessEntities( return []; } - if (userStartNodePaths.Any(path => $"{targetParent?.Path},".StartsWith($"{path},"))) + if (userStartNodePaths.Any(path => $"{targetParent.Path},".StartsWith($"{path},"))) { // The requested parent of the target is one of the user start nodes (or a descendant of one), all siblings are by definition allowed. - siblings = _entityService.GetSiblings(targetKey, [umbracoObjectType], before, after, out totalBefore, out totalAfter, ordering: ordering).ToArray(); - return ChildUserAccessEntities(siblings, userStartNodePaths); + IEnumerable allSiblings = _entityService.GetSiblings(targetKey, umbracoObjectTypes, before, after, out totalBefore, out totalAfter, ordering: ordering); + + return ChildUserAccessEntities(allSiblings, userStartNodePaths); } List allowedSiblingIds = GetAllowedIds(userStartNodePaths, targetParent.Id); @@ -205,10 +243,11 @@ public IEnumerable SiblingUserAccessEntities( return []; } - // Even though we know the IDs of the allowed sibling entities to fetch, we still use a Query to yield correctly sorted children. + // Fetch allowed siblings from all object types IQuery query = _scopeProvider.CreateQuery().Where(x => allowedSiblingIds.Contains(x.Id)); - siblings = _entityService.GetSiblings(targetKey, [umbracoObjectType], before, after, out totalBefore, out totalAfter, query, ordering).ToArray(); - return ChildUserAccessEntities(siblings, userStartNodePaths); + IEnumerable allAllowedSiblings = _entityService.GetSiblings(targetKey, umbracoObjectTypes, before, after, out totalBefore, out totalAfter, query, ordering); + + return ChildUserAccessEntities(allAllowedSiblings, userStartNodePaths); } /// diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/FolderTreeItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/FolderTreeItemResponseModel.cs index c92da67ce4d5..d2dc9880e438 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/FolderTreeItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/FolderTreeItemResponseModel.cs @@ -3,4 +3,6 @@ public class FolderTreeItemResponseModel : NamedEntityTreeItemResponseModel { public bool IsFolder { get; set; } + + public bool NoAccess { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs index 8cc71e84822a..889bd82376a9 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/CalculatedUserStartNodesResponseModel.cs @@ -11,4 +11,8 @@ public class CalculatedUserStartNodesResponseModel public ISet MediaStartNodeIds { get; set; } = new HashSet(); public bool HasMediaRootAccess { get; set; } + + public ISet ElementStartNodeIds { get; set; } = new HashSet(); + + public bool HasElementRootAccess { get; set; } } 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 332e1768d51c..ce4dbd460434 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrentUserResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/Current/CurrentUserResponseModel.cs @@ -16,6 +16,10 @@ public class CurrentUserResponseModel : UserPresentationBase public required bool HasMediaRootAccess { get; init; } + public required ISet ElementStartNodeIds { get; init; } = new HashSet(); + + public required bool HasElementRootAccess { get; init; } + public required IEnumerable AvatarUrls { get; init; } = Enumerable.Empty(); public required IEnumerable Languages { get; init; } = Enumerable.Empty(); diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/UpdateUserRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/UpdateUserRequestModel.cs index ea65567e23a6..31079f731737 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/UpdateUserRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/UpdateUserRequestModel.cs @@ -11,4 +11,8 @@ public class UpdateUserRequestModel : UserPresentationBase public ISet MediaStartNodeIds { get; set; } = new HashSet(); public bool HasMediaRootAccess { get; init; } + + public ISet ElementStartNodeIds { get; set; } = new HashSet(); + + public bool HasElementRootAccess { get; init; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs index 8177b02d1e5f..726831623faf 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs @@ -16,6 +16,10 @@ public class UserResponseModel : UserPresentationBase public bool HasMediaRootAccess { get; set; } + public ISet ElementStartNodeIds { get; set; } = new HashSet(); + + public bool HasElementRootAccess { get; set; } + public IEnumerable AvatarUrls { get; set; } = Enumerable.Empty(); public UserState State { 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 a8d46405ee7c..bed21a19b896 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs @@ -79,6 +79,22 @@ public class UserGroupBase /// public bool MediaRootAccess { get; init; } + /// + /// The key of the element that should act as root node for the user group + /// + /// This can be overwritten by a different user group if a user is a member of multiple groups + /// + /// + public ReferenceByIdModel? ElementStartNode { get; init; } + + /// + /// If the group should have access to the element root. + /// + /// This will be ignored if an explicit start node has been specified in . + /// + /// + public bool ElementRootAccess { get; init; } + /// /// 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. /// diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 0e75b6820db5..ca6c86279f0e 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -14,8 +14,10 @@ public static class CacheKeys public const string UserAllContentStartNodesPrefix = "AllContentStartNodes"; public const string UserAllMediaStartNodesPrefix = "AllMediaStartNodes"; + public const string UserAllElementStartNodesPrefix = "AllElementStartNodes"; public const string UserMediaStartNodePathsPrefix = "MediaStartNodePaths"; public const string UserContentStartNodePathsPrefix = "ContentStartNodePaths"; + public const string UserElementStartNodePathsPrefix = "ElementStartNodePaths"; public const string ContentRecycleBinCacheKey = "recycleBin_content"; public const string MediaRecycleBinCacheKey = "recycleBin_media"; diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index 4a53ff455f54..b0f2e75a3942 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -46,6 +46,11 @@ public static class Applications /// Application alias for the forms section. /// public const string Forms = "forms"; + + /// + /// Application alias for the library section. + /// + public const string Library = "library"; } /// diff --git a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs index 9799412b453e..753e641a81eb 100644 --- a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs @@ -24,6 +24,8 @@ public interface IReadOnlyUserGroup int? StartMediaId { get; } + int? StartElementId { get; } + // This is set to return true as default to avoid breaking changes. bool HasAccessToAllLanguages => true; diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index 868daee42609..829a0f3c9dfc 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -18,6 +18,8 @@ public interface IUser : IMembershipUser, IRememberBeingDirty int[]? StartMediaIds { get; set; } + int[]? StartElementIds { get; set; } + string? Language { get; set; } DateTime? InvitedDate { get; set; } diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index a2dd356be84c..79f74fcb1a91 100644 --- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -24,6 +24,8 @@ public interface IUserGroup : IEntity, IRememberBeingDirty /// int? StartMediaId { get; set; } + int? StartElementId { get; set; } + /// /// The icon /// diff --git a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs index 407edf23b4d2..8a96e8bdf7cb 100644 --- a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs @@ -26,6 +26,40 @@ public ReadOnlyUserGroup( icon, startContentId, startMediaId, + null, + alias, + allowedLanguages, + allowedSections, + permissions, + granularPermissions, + hasAccessToAllLanguages) + { + } + + [Obsolete("Please use the constructor that includes all parameters. Scheduled for removal in Umbraco 19.")] + public ReadOnlyUserGroup( + int id, + Guid key, + string? name, + string? icon, + int? startContentId, + int? startMediaId, + int? startElementId, + string? alias, + IEnumerable allowedLanguages, + IEnumerable allowedSections, + ISet permissions, + ISet granularPermissions, + bool hasAccessToAllLanguages) + : this( + id, + key, + name, + null, + icon, + startContentId, + startMediaId, + startElementId, alias, allowedLanguages, allowedSections, @@ -43,6 +77,7 @@ public ReadOnlyUserGroup( string? icon, int? startContentId, int? startMediaId, + int? startElementId, string? alias, IEnumerable allowedLanguages, IEnumerable allowedSections, @@ -62,6 +97,7 @@ public ReadOnlyUserGroup( // Zero is invalid and will be treated as Null StartContentId = startContentId == 0 ? null : startContentId; StartMediaId = startMediaId == 0 ? null : startMediaId; + StartElementId = startElementId == 0 ? null : startElementId; HasAccessToAllLanguages = hasAccessToAllLanguages; Permissions = permissions; GranularPermissions = granularPermissions; @@ -81,6 +117,8 @@ public ReadOnlyUserGroup( public int? StartMediaId { get; } + public int? StartElementId { get; } + public string Alias { get; } public bool HasAccessToAllLanguages { get; set; } diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 571449dcc38b..a77685ec7e35 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -38,14 +38,16 @@ public class User : EntityBase, IUser, IProfile private int _sessionTimeout; private int[]? _startContentIds; private int[]? _startMediaIds; + private int[]? _startElementIds; private HashSet _userGroups; private string _username; private UserKind _kind; /// - /// Constructor for creating a new/empty user + /// Initializes a new instance of the class for a new/empty user. /// + /// The global settings. public User(GlobalSettings globalSettings) { SessionTimeout = 60; @@ -55,6 +57,7 @@ public User(GlobalSettings globalSettings) _isLockedOut = false; _startContentIds = []; _startMediaIds = []; + _startElementIds = []; // cannot be null _rawPasswordValue = string.Empty; @@ -64,13 +67,13 @@ public User(GlobalSettings globalSettings) } /// - /// Constructor for creating a new/empty user + /// Initializes a new instance of the class for a new/empty user. /// - /// - /// - /// - /// - /// + /// The global settings. + /// The name. + /// The email. + /// The username. + /// The raw password value. public User(GlobalSettings globalSettings, string? name, string email, string username, string rawPasswordValue) : this(globalSettings) { @@ -103,21 +106,23 @@ public User(GlobalSettings globalSettings, string? name, string email, string us _isLockedOut = false; _startContentIds = []; _startMediaIds = []; + _startElementIds = []; } /// - /// Constructor for creating a new User instance for an existing user + /// Initializes a new instance of the class for an existing user. /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + /// The global settings. + /// The identifier. + /// The name. + /// The email. + /// The username. + /// The raw password value. + /// The password configuration. + /// The user groups. + /// The start content identifiers. + /// The start media identifiers. + [Obsolete("Use the constructor that includes startElementIds. Scheduled for removal in Umbraco 19.")] public User( GlobalSettings globalSettings, int id, @@ -129,6 +134,36 @@ public User( IEnumerable userGroups, int[] startContentIds, int[] startMediaIds) + : this(globalSettings, id, name, email, username, rawPasswordValue, passwordConfig, userGroups, startContentIds, startMediaIds, []) + { + } + + /// + /// Initializes a new instance of the class for an existing user. + /// + /// The global settings. + /// The identifier. + /// The name. + /// The email. + /// The username. + /// The raw password value. + /// The password configuration. + /// The user groups. + /// The start content identifiers. + /// The start media identifiers. + /// The start element identifiers. + public User( + GlobalSettings globalSettings, + int id, + string? name, + string email, + string? username, + string? rawPasswordValue, + string? passwordConfig, + IEnumerable userGroups, + int[] startContentIds, + int[] startMediaIds, + int[] startElementIds) : this(globalSettings) { // we allow whitespace for this value so just check null @@ -163,6 +198,7 @@ public User( _isLockedOut = false; _startContentIds = startContentIds ?? throw new ArgumentNullException(nameof(startContentIds)); _startMediaIds = startMediaIds ?? throw new ArgumentNullException(nameof(startMediaIds)); + _startElementIds = startElementIds ?? throw new ArgumentNullException(nameof(startElementIds)); } [DataMember] @@ -351,6 +387,20 @@ public int[]? StartMediaIds set => SetPropertyValueAndDetectChanges(value, ref _startMediaIds, nameof(StartMediaIds), IntegerEnumerableComparer); } + /// + /// Gets or sets the start element ids. + /// + /// + /// The start element ids. + /// + [DataMember] + [DoNotClone] + public int[]? StartElementIds + { + get => _startElementIds; + set => SetPropertyValueAndDetectChanges(value, ref _startElementIds, nameof(StartElementIds), IntegerEnumerableComparer); + } + [DataMember] public string? Language { @@ -417,6 +467,7 @@ protected override void PerformDeepClone(object clone) // manually clone the start node props clonedEntity._startContentIds = _startContentIds?.ToArray(); clonedEntity._startMediaIds = _startMediaIds?.ToArray(); + clonedEntity._startElementIds = _startElementIds?.ToArray(); // need to create new collections otherwise they'll get copied by ref clonedEntity._userGroups = new HashSet(_userGroups); diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index 0b500f9bd3f6..452be1613d77 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -38,6 +38,7 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup private List _languageCollection; private int? _startContentId; private int? _startMediaId; + private int? _startElementId; /// /// Constructor to create a new user group @@ -82,6 +83,13 @@ public int? StartMediaId set => SetPropertyValueAndDetectChanges(value, ref _startMediaId, nameof(StartMediaId)); } + [DataMember] + public int? StartElementId + { + get => _startElementId; + set => SetPropertyValueAndDetectChanges(value, ref _startElementId, nameof(StartElementId)); + } + [DataMember] public int? StartContentId { diff --git a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs index cd6db258970f..cb5580eb07f3 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs @@ -22,6 +22,7 @@ public static IReadOnlyUserGroup ToReadOnlyGroup(this IUserGroup group) group.Icon, group.StartContentId, group.StartMediaId, + group.StartElementId, group.Alias, group.AllowedLanguages, group.AllowedSections, diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index f1f4064847a6..59353238b8b9 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -93,6 +93,18 @@ internal static bool HasMediaBinAccess(this IUser user, IEntityService entitySer user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); + internal static bool HasElementRootAccess(this IUser user, IEntityService entityService, AppCaches appCaches) => + ContentPermissions.HasPathAccess( + Constants.System.RootString, + user.CalculateElementStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinElement); + + internal static bool HasElementBinAccess(this IUser user, IEntityService entityService, AppCaches appCaches) => + ContentPermissions.HasPathAccess( + Constants.System.RecycleBinElementString, + user.CalculateElementStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinElement); + public static bool HasPathAccess(this IUser user, IMedia? media, IEntityService entityService, AppCaches appCaches) { if (media == null) @@ -126,6 +138,19 @@ public static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IE return ContentPermissions.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService, appCaches), Constants.System.RecycleBinMedia); } + public static bool HasElementPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, AppCaches appCaches) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + return ContentPermissions.HasPathAccess( + entity.Path, + user.CalculateElementStartNodeIds(entityService, appCaches), + Constants.System.RecycleBinElement); + } + /// /// Determines whether this user has access to view sensitive data /// @@ -211,6 +236,38 @@ public static int[] CalculateAllowedLanguageIds(this IUser user, ILocalizationSe return result; } + /// + /// Calculate start nodes, combining groups' and user's, and excluding what's in the bin + /// + /// + /// + /// + /// + public static int[]? CalculateElementStartNodeIds(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = user.UserCacheKey(CacheKeys.UserAllElementStartNodesPrefix); + IAppPolicyCache runtimeCache = GetUserCache(appCaches); + var result = runtimeCache.GetCacheItem( + cacheKey, + () => + { + var gsn = user.Groups.Where(x => x.StartElementId.HasValue).Select(x => x.StartElementId!.Value).Distinct() + .ToArray(); + var usn = user.StartElementIds; + if (usn is not null) + { + var vals = CombineStartNodes(UmbracoObjectTypes.ElementContainer, gsn, usn, entityService); + return vals; + } + + return null; + }, + TimeSpan.FromMinutes(2), + true); + + return result; + } + public static string[]? GetMediaStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) { var cacheKey = user.UserCacheKey(CacheKeys.UserMediaStartNodePathsPrefix); @@ -248,6 +305,24 @@ public static int[] CalculateAllowedLanguageIds(this IUser user, ILocalizationSe return result; } + public static string[]? GetElementStartNodePaths(this IUser user, IEntityService entityService, AppCaches appCaches) + { + var cacheKey = user.UserCacheKey(CacheKeys.UserElementStartNodePathsPrefix); + IAppPolicyCache runtimeCache = GetUserCache(appCaches); + var result = runtimeCache.GetCacheItem( + cacheKey, + () => + { + var startNodeIds = user.CalculateElementStartNodeIds(entityService, appCaches); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.ElementContainer, startNodeIds).Select(x => x.Path).ToArray(); + return vals; + }, + TimeSpan.FromMinutes(2), + true); + + return result; + } + internal static int[] CombineStartNodes(UmbracoObjectTypes objectType, int[] groupSn, int[] userSn, IEntityService entityService) { // assume groupSn and userSn each don't contain duplicates @@ -336,6 +411,10 @@ private static string GetBinPath(UmbracoObjectTypes objectType) case UmbracoObjectTypes.Media: binPath += Constants.System.RecycleBinMediaString; break; + case UmbracoObjectTypes.Element: + case UmbracoObjectTypes.ElementContainer: + binPath += Constants.System.RecycleBinElementString; + break; default: throw new ArgumentOutOfRangeException(nameof(objectType)); } diff --git a/src/Umbraco.Core/Models/UserUpdateModel.cs b/src/Umbraco.Core/Models/UserUpdateModel.cs index 684cbafda891..53c59106e60a 100644 --- a/src/Umbraco.Core/Models/UserUpdateModel.cs +++ b/src/Umbraco.Core/Models/UserUpdateModel.cs @@ -20,5 +20,9 @@ public class UserUpdateModel public bool HasMediaRootAccess { get; set; } + public ISet ElementStartNodeKeys { get; set; } = new HashSet(); + + public bool HasElementRootAccess { get; set; } + public ISet UserGroupKeys { get; set; } = new HashSet(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index 9f8640c3c258..2c36d5193e5e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -17,6 +17,15 @@ public interface IEntityRepository : IRepository IEnumerable GetAll(Guid objectType, params int[] ids); + /// + /// Gets entities of multiple object types. + /// + /// The object types of the entities. + /// The identifiers of the entities. + /// If is empty, returns all entities of the specified types. + IEnumerable GetAll(IEnumerable objectTypes, params int[] ids) + => throw new NotImplementedException(); + IEnumerable GetAll(Guid objectType, params Guid[] keys); /// diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 3a319a2a7b77..d95a87b0dfee 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -192,6 +192,17 @@ public virtual IEnumerable GetAll(UmbracoObjectTypes objectType, pa } } + /// + public virtual IEnumerable GetAll(IEnumerable objectTypes, params int[] ids) + { + IEnumerable objectTypeGuids = objectTypes.Select(x => x.GetGuid()); + + using (ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return _entityRepository.GetAll(objectTypeGuids, ids); + } + } + /// public virtual IEnumerable GetAll(Guid objectType) => GetAll(objectType, Array.Empty()); @@ -753,9 +764,6 @@ public Attempt GetKey(int id, UmbracoObjectTypes umbracoObjectType) => /// public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params int[]? ids) { - Type? entityType = objectType.GetClrType(); - GetObjectType(entityType); - using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetAllPaths(objectType.GetGuid(), ids); @@ -765,9 +773,6 @@ public virtual IEnumerable GetAllPaths(UmbracoObjectTypes object /// public virtual IEnumerable GetAllPaths(UmbracoObjectTypes objectType, params Guid[] keys) { - Type? entityType = objectType.GetClrType(); - GetObjectType(entityType); - using (ScopeProvider.CreateCoreScope(autoComplete: true)) { return _entityRepository.GetAllPaths(objectType.GetGuid(), keys); @@ -784,7 +789,7 @@ public int ReserveId(Guid key) } private int CountChildren(int id, UmbracoObjectTypes objectType, bool trashed = false, IQuery? filter = null) => - CountChildren(id, new HashSet() { objectType }, trashed, filter); + CountChildren(id, new HashSet { objectType }, trashed, filter); private int CountChildren( int id, diff --git a/src/Umbraco.Core/Services/IElementService.cs b/src/Umbraco.Core/Services/IElementService.cs index 473db8bfccdc..ef189c35d97c 100644 --- a/src/Umbraco.Core/Services/IElementService.cs +++ b/src/Umbraco.Core/Services/IElementService.cs @@ -13,4 +13,11 @@ public interface IElementService : IPublishableContentService /// The identifier of the user performing the action. /// The created element. IElement Create(string name, string contentTypeAlias, int userId = Constants.Security.SuperUserId); + + /// + /// Gets elements. + /// + /// The identifiers of the elements. + /// The elements. + IEnumerable GetByIds(IEnumerable keys); } diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index cfeb0d69c84e..3988ec66fe8f 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -112,6 +112,15 @@ IEnumerable GetAll(params int[] ids) /// If is empty, returns all entities. IEnumerable GetAll(UmbracoObjectTypes objectType, params int[] ids); + /// + /// Gets entities of multiple object types. + /// + /// The object types of the entities. + /// The identifiers of the entities. + /// If is empty, returns all entities of the specified types. + IEnumerable GetAll(IEnumerable objectTypes, params int[] ids) + => throw new NotImplementedException(); + /// /// Gets entities of a given object type. /// diff --git a/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs index d658e07565e0..3180058ec6ba 100644 --- a/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs @@ -22,4 +22,5 @@ public enum UserGroupOperationStatus Unauthorized, AdminGroupCannotBeEmpty, UserNotInGroup, + ElementStartNodeKeyNotFound, } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 41f52c2c6c33..6376a5cc8bcc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1326,6 +1326,7 @@ private void CreateUserGroupData() Key = Constants.Security.AdminGroupKey, StartMediaId = -1, StartContentId = -1, + StartElementId = -1, Alias = Constants.Security.AdminGroupAlias, Name = "Administrators", Description = "Users with full access to all sections and functionality", @@ -1344,6 +1345,7 @@ private void CreateUserGroupData() Key = Constants.Security.WriterGroupKey, StartMediaId = -1, StartContentId = -1, + StartElementId = -1, Alias = WriterGroupAlias, Name = "Writers", Description = "Users with permission to create and update but not publish content", @@ -1362,6 +1364,7 @@ private void CreateUserGroupData() Key = Constants.Security.EditorGroupKey, StartMediaId = -1, StartContentId = -1, + StartElementId = -1, Alias = EditorGroupAlias, Name = "Editors", Description = "Users with full permission to create, update and publish content", @@ -1380,6 +1383,7 @@ private void CreateUserGroupData() Key = Constants.Security.TranslatorGroupKey, StartMediaId = -1, StartContentId = -1, + StartElementId = -1, Alias = TranslatorGroupAlias, Name = "Translators", Description = "Users with permission to manage dictionary entries", @@ -1430,12 +1434,15 @@ private void CreateUserGroup2AppData() _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Users }); _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Forms }); _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Translation }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Library }); _database.Insert(new UserGroup2AppDto { UserGroupId = 2, AppAlias = Constants.Applications.Content }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 2, AppAlias = Constants.Applications.Library }); _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Constants.Applications.Content }); _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Constants.Applications.Media }); _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Constants.Applications.Forms }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Constants.Applications.Library }); _database.Insert(new UserGroup2AppDto { UserGroupId = 4, AppAlias = Constants.Applications.Translation }); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/AddElements.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/AddElements.cs index 41712563235a..1e785149cdde 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/AddElements.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_18_0_0/AddElements.cs @@ -18,6 +18,7 @@ protected override Task MigrateAsync() EnsureElementTreeLock(); EnsureElementTables(); EnsureElementRecycleBin(); + EnsureElementStartNodeColumn(); return Task.CompletedTask; } @@ -101,4 +102,15 @@ private void ToggleIdentityInsertForNodes(bool toggleOn) Database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(NodeDto.TableName)} {(toggleOn ? "ON" : "OFF")} ")); } } + + private void EnsureElementStartNodeColumn() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + + if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.UserGroup) + && x.ColumnName.InvariantEquals("startElementId")) == false) + { + AddColumn(Constants.DatabaseSchema.Tables.UserGroup, "startElementId"); + } + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs index 049c3fc940a9..17c0234fbb0b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs @@ -79,6 +79,11 @@ public UserGroupDto() [ForeignKey(typeof(NodeDto), Name = "FK_startMediaId_umbracoNode_id")] public int? StartMediaId { get; set; } + [Column("startElementId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startElementId_umbracoNode_id")] + public int? StartElementId { get; set; } + [ResultColumn] [Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")] public List UserGroup2AppDtos { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs index 51ca08b84209..3bfff7aac5f6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs @@ -15,6 +15,7 @@ public enum StartNodeTypeValue { Content = 1, Media = 2, + Element = 3, } [Column("id")] diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs index 1274d958c3da..cdc440a65550 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -34,6 +34,8 @@ public static IUser BuildEntity( 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) + .Select(x => x.StartNode).ToArray(), + dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Element) .Select(x => x.StartNode).ToArray()); try @@ -91,7 +93,7 @@ public static UserDto BuildDto(IUser entity) Avatar = entity.Avatar, EmailConfirmedDate = entity.EmailConfirmedDate, InvitedDate = entity.InvitedDate, - Kind = (short)entity.Kind + Kind = (short)entity.Kind, }; if (entity.StartContentIds is not null) @@ -120,6 +122,19 @@ public static UserDto BuildDto(IUser entity) } } + if (entity.StartElementIds is not null) + { + foreach (var startNodeId in entity.StartElementIds) + { + dto.UserStartNodeDtos.Add(new UserStartNodeDto + { + StartNode = startNodeId, + StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Element, + UserId = entity.Id, + }); + } + } + if (entity.HasIdentity) { dto.Id = entity.Id; @@ -128,15 +143,16 @@ public static UserDto BuildDto(IUser entity) return dto; } - private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group, IDictionary permissionMappers) - { - return new ReadOnlyUserGroup( + private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group, IDictionary permissionMappers) => + new ReadOnlyUserGroup( group.Id, group.Key, group.Name, + group.Description, group.Icon, group.StartContentId, group.StartMediaId, + group.StartElementId, group.Alias, group.UserGroup2LanguageDtos.Select(x => x.LanguageId), group.UserGroup2AppDtos.Select(x => x.AppAlias).WhereNotNull().ToArray(), @@ -148,12 +164,11 @@ private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group, IDictionar return mapper.MapFromDto(granularPermission); } - return new UnknownTypeGranularPermission() + return new UnknownTypeGranularPermission { Permission = granularPermission.Permission, - Context = granularPermission.Context + Context = granularPermission.Context, }; })), group.HasAccessToAllLanguages); - } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs index 4d715c4ed840..11d08cb8161d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs @@ -28,6 +28,7 @@ public static IUserGroup BuildEntity(IShortStringHelper shortStringHelper, UserG userGroup.UpdateDate = dto.UpdateDate.EnsureUtc(); userGroup.StartContentId = dto.StartContentId; userGroup.StartMediaId = dto.StartMediaId; + userGroup.StartElementId = dto.StartElementId; userGroup.Permissions = dto.UserGroup2PermissionDtos.Select(x => x.Permission).ToHashSet(); userGroup.HasAccessToAllLanguages = dto.HasAccessToAllLanguages; userGroup.Description = dto.Description; @@ -92,6 +93,7 @@ public static UserGroupDto BuildDto(IUserGroup entity) Icon = entity.Icon, StartMediaId = entity.StartMediaId, StartContentId = entity.StartContentId, + StartElementId = entity.StartElementId, HasAccessToAllLanguages = entity.HasAccessToAllLanguages, }; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index 4996c9c80860..53ca9c34d6e5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -407,6 +407,14 @@ public IEnumerable GetAll(Guid objectType, params int[] ids) => ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) : PerformGetAll(objectType); + public IEnumerable GetAll(IEnumerable objectTypes, params int[] ids) + { + Guid[] objectTypeArray = objectTypes.ToArray(); + return ids.Length > 0 + ? PerformGetAll(objectTypeArray, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) + : PerformGetAll(objectTypeArray); + } + public IEnumerable GetAll(Guid objectType, params Guid[] keys) => keys.Length > 0 ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) @@ -454,6 +462,18 @@ private IEnumerable PerformGetAll(Guid objectType, Action PerformGetAll(Guid[] objectTypes, Action>? filter = null) + { + var isContent = objectTypes.Contains(Constants.ObjectTypes.Document) || + objectTypes.Contains(Constants.ObjectTypes.DocumentBlueprint); + var isMedia = objectTypes.Contains(Constants.ObjectTypes.Media); + var isMember = objectTypes.Contains(Constants.ObjectTypes.Member); + var isElement = objectTypes.Contains(Constants.ObjectTypes.Element); + + Sql sql = GetFullSqlForEntityType(isContent, isMedia, isMember, isElement, objectTypes, filter); + return GetEntities(sql, isContent, isMedia, isMember, isElement); + } + private IEnumerable PerformGetAll( Guid[] objectTypes, Ordering ordering, @@ -760,7 +780,20 @@ private Sql GetFullSqlForEntityType( Guid objectType, Action>? filter) { - Sql sql = GetBaseWhere(isContent, isMedia, isMember, isElement, false, filter, new[] { objectType }); + Sql sql = GetBaseWhere(isContent, isMedia, isMember, isElement, false, filter, [objectType]); + return AddGroupBy(isContent, isMedia, isMember, isElement, sql, true); + } + + // gets the full sql for multiple object types, with a given filter + private Sql GetFullSqlForEntityType( + bool isContent, + bool isMedia, + bool isMember, + bool isElement, + Guid[] objectTypes, + Action>? filter) + { + Sql sql = GetBaseWhere(isContent, isMedia, isMember, isElement, false, filter, objectTypes); return AddGroupBy(isContent, isMedia, isMember, isElement, sql, true); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs index 2a5e31d50f2c..969d20df251f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -478,6 +478,7 @@ private static void AppendGroupBy(Sql sql) => x => x.Id, x => x.StartContentId, x => x.StartMediaId, + x => x.StartElementId, x => x.UpdateDate, x => x.Alias, x => x.Name, diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 1de89e8a09fe..ae766f7dbd94 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -741,6 +741,15 @@ protected override void PersistNewItem(IUser entity) entity.StartMediaIds); } + if (entity.IsPropertyDirty("StartElementIds")) + { + AddingOrUpdateStartNodes( + entity, + Enumerable.Empty(), + UserStartNodeDto.StartNodeTypeValue.Element, + entity.StartElementIds); + } + if (entity.IsPropertyDirty("Groups")) { // Lookup all assigned groups. @@ -859,7 +868,7 @@ protected override void PersistUpdatedItem(IUser entity) Database.Update(userDto, changedCols); } - if (entity.IsPropertyDirty("StartContentIds") || entity.IsPropertyDirty("StartMediaIds")) + if (entity.IsPropertyDirty("StartContentIds") || entity.IsPropertyDirty("StartMediaIds") || entity.IsPropertyDirty("StartElementIds")) { Sql sql = SqlContext.Sql() .SelectAll() @@ -877,6 +886,11 @@ protected override void PersistUpdatedItem(IUser entity) { AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); } + + if (entity.IsPropertyDirty("StartElementIds")) + { + AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Element, entity.StartElementIds); + } } if (entity.IsPropertyDirty("Groups")) diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index 3a203e3ad906..3b9aa7e5d383 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -25,15 +25,18 @@ public static class AuthorizationPolicies public const string SectionAccessMedia = nameof(SectionAccessMedia); public const string SectionAccessSettings = nameof(SectionAccessSettings); public const string SectionAccessMembers = nameof(SectionAccessMembers); + public const string SectionAccessLibrary = nameof(SectionAccessLibrary); // Custom access based on multiple sections public const string SectionAccessContentOrMedia = nameof(SectionAccessContentOrMedia); public const string SectionAccessForMemberTree = nameof(SectionAccessForMemberTree); public const string SectionAccessForMediaTree = nameof(SectionAccessForMediaTree); public const string SectionAccessForContentTree = nameof(SectionAccessForContentTree); + public const string SectionAccessForElementTree = nameof(SectionAccessForElementTree); // Single tree access public const string TreeAccessDocuments = nameof(TreeAccessDocuments); + public const string TreeAccessElements = nameof(TreeAccessElements); public const string TreeAccessPartialViews = nameof(TreeAccessPartialViews); public const string TreeAccessDataTypes = nameof(TreeAccessDataTypes); public const string TreeAccessWebhooks = nameof(TreeAccessWebhooks); diff --git a/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs index 2a00680ab73d..5e2a4355b092 100644 --- a/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.Globalization; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; diff --git a/tests/Umbraco.Tests.Common/Builders/UserBuilder.cs b/tests/Umbraco.Tests.Common/Builders/UserBuilder.cs index 529b186967bc..703abfc20f11 100644 --- a/tests/Umbraco.Tests.Common/Builders/UserBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/UserBuilder.cs @@ -48,6 +48,7 @@ public class UserBuilder private int? _sessionTimeout; private int[] _startContentIds; private int[] _startMediaIds; + private int[] _startElementIds; private string _suffix = string.Empty; private DateTime? _updateDate; private string _username; @@ -197,6 +198,18 @@ public UserBuilder WithStartMediaIds(int[] startMediaIds) return this; } + public UserBuilder WithStartElementId(int startElementId) + { + _startElementIds = new[] { startElementId }; + return this; + } + + public UserBuilder WithStartElementIds(int[] startElementIds) + { + _startElementIds = startElementIds; + return this; + } + public UserBuilder WithSuffix(string suffix) { _suffix = suffix; @@ -233,6 +246,7 @@ public override User Build() var sessionTimeout = _sessionTimeout ?? 0; var startContentIds = _startContentIds ?? new[] { -1 }; var startMediaIds = _startMediaIds ?? new[] { -1 }; + var startElementIds = _startElementIds ?? new[] { -1 }; var groups = _userGroupBuilders.Select(x => x.Build()); var result = new User( @@ -256,7 +270,8 @@ public override User Build() Comments = comments, SessionTimeout = sessionTimeout, StartContentIds = startContentIds, - StartMediaIds = startMediaIds + StartMediaIds = startMediaIds, + StartElementIds = startElementIds }; foreach (var readOnlyUserGroup in groups) { diff --git a/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs b/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs index b06c97b41989..c7af53299aaf 100644 --- a/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs @@ -37,6 +37,7 @@ public class UserGroupBuilder private ISet _permissions = new HashSet(); private int? _startContentId; private int? _startMediaId; + private int? _startElementId; private string _suffix; private int? _userCount; @@ -128,6 +129,12 @@ public UserGroupBuilder WithStartMediaId(int startMediaId) return this; } + public UserGroupBuilder WithStartElementId(int startElementId) + { + _startElementId = startElementId; + return this; + } + public IReadOnlyUserGroup BuildReadOnly(IUserGroup userGroup) => Mock.Of(x => x.Permissions == userGroup.Permissions && @@ -136,6 +143,7 @@ public IReadOnlyUserGroup BuildReadOnly(IUserGroup userGroup) => x.Name == userGroup.Name && x.StartContentId == userGroup.StartContentId && x.StartMediaId == userGroup.StartMediaId && + x.StartElementId == userGroup.StartElementId && x.AllowedSections == userGroup.AllowedSections && x.Id == userGroup.Id && x.Key == userGroup.Key); @@ -149,6 +157,7 @@ public override IUserGroup Build() var userCount = _userCount ?? 0; var startContentId = _startContentId ?? -1; var startMediaId = _startMediaId ?? -1; + var startElementId = _startElementId ?? -1; var icon = _icon ?? "icon-group"; var shortStringHelper = new DefaultShortStringHelper(new DefaultShortStringHelperConfig()); @@ -159,6 +168,7 @@ public override IUserGroup Build() Key = key, StartContentId = startContentId, StartMediaId = startMediaId, + StartElementId = startElementId, Permissions = _permissions, }; diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/ByKeyElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/ByKeyElementControllerTests.cs new file mode 100644 index 000000000000..3d6e6323fffc --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/ByKeyElementControllerTests.cs @@ -0,0 +1,63 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class ByKeyElementControllerTests : ManagementApiUserGroupTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementKey; + + [SetUp] + public async Task Setup() + { + // Create element type + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + // Create element + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = "Test Element Instance" }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + } + + protected override Expression> MethodSelector => + x => x.ByKey(CancellationToken.None, _elementKey); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/ConfigurationElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/ConfigurationElementControllerTests.cs new file mode 100644 index 000000000000..8d43f483157d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/ConfigurationElementControllerTests.cs @@ -0,0 +1,29 @@ +using System.Linq.Expressions; +using System.Net; +using Umbraco.Cms.Api.Management.Controllers.Element; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class ConfigurationElementControllerTests : ManagementApiUserGroupTestBase +{ + protected override Expression> MethodSelector => + x => x.Configuration(CancellationToken.None); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/CopyElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/CopyElementControllerTests.cs new file mode 100644 index 000000000000..b90d8f334355 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/CopyElementControllerTests.cs @@ -0,0 +1,83 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element; +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Element; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class CopyElementControllerTests : ManagementApiUserGroupTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IElementContainerService ElementContainerService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementKey; + private Guid _targetContainerKey; + + [SetUp] + public async Task Setup() + { + // Create element type + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + // Create element to copy + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = "Element to Copy" }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + + // Create target container + var targetResult = await ElementContainerService.CreateAsync(null, Guid.NewGuid().ToString(), null, Constants.Security.SuperUserKey); + _targetContainerKey = targetResult.Result!.Key; + } + + protected override Expression> MethodSelector => + x => x.Copy(CancellationToken.None, _elementKey, null!); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Created }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Created }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Created }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + { + var copyElementRequestModel = new CopyElementRequestModel + { + Target = new ReferenceByIdModel(_targetContainerKey), + }; + + return await Client.PostAsync(Url, JsonContent.Create(copyElementRequestModel)); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/CreateElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/CreateElementControllerTests.cs new file mode 100644 index 000000000000..f2287b517bad --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/CreateElementControllerTests.cs @@ -0,0 +1,71 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element; +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Element; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class CreateElementControllerTests : ManagementApiUserGroupTestBase +{ + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementTypeKey; + + [SetUp] + public async Task Setup() + { + // Create element type + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + _elementTypeKey = elementType.Key; + } + + protected override Expression> MethodSelector => + x => x.Create(CancellationToken.None, null!); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Created }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Created }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Created }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + { + var createElementRequestModel = new CreateElementRequestModel + { + DocumentType = new ReferenceByIdModel(_elementTypeKey), + Parent = null, + Id = Guid.NewGuid(), + Values = [], + Variants = + [ + new ElementVariantRequestModel { Culture = null, Segment = null, Name = "Test Element Instance" } + ], + }; + + return await Client.PostAsync(Url, JsonContent.Create(createElementRequestModel)); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/DeleteElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/DeleteElementControllerTests.cs new file mode 100644 index 000000000000..b9c1213a4925 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/DeleteElementControllerTests.cs @@ -0,0 +1,67 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class DeleteElementControllerTests : ManagementApiUserGroupTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementTypeKey; + private Guid _elementKey; + + [SetUp] + public async Task Setup() + { + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + _elementTypeKey = elementType.Key; + + // Create a new element for each test since delete removes it + var createModel = new ElementCreateModel + { + ContentTypeKey = _elementTypeKey, + ParentKey = null, + Variants = [new VariantModel { Name = Guid.NewGuid().ToString() }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + } + + protected override Expression> MethodSelector => + x => x.Delete(CancellationToken.None, _elementKey); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + => await Client.DeleteAsync(Url); +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/ByKeyElementFolderControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/ByKeyElementFolderControllerTests.cs new file mode 100644 index 000000000000..c00909acc19d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/ByKeyElementFolderControllerTests.cs @@ -0,0 +1,43 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.Folder; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.Folder; + +public class ByKeyElementFolderControllerTests : ManagementApiUserGroupTestBase +{ + private IElementContainerService ElementContainerService => GetRequiredService(); + + private Guid _folderKey; + + [SetUp] + public async Task Setup() + { + var result = await ElementContainerService.CreateAsync(null, Guid.NewGuid().ToString(), null, Constants.Security.SuperUserKey); + _folderKey = result.Result!.Key; + } + + protected override Expression> MethodSelector => + x => x.ByKey(CancellationToken.None, _folderKey); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/CreateElementFolderControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/CreateElementFolderControllerTests.cs new file mode 100644 index 000000000000..d806ce377bcb --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/CreateElementFolderControllerTests.cs @@ -0,0 +1,43 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using Umbraco.Cms.Api.Management.Controllers.Element.Folder; +using Umbraco.Cms.Api.Management.ViewModels.Folder; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.Folder; + +public class CreateElementFolderControllerTests : ManagementApiUserGroupTestBase +{ + protected override Expression> MethodSelector => + x => x.Create(CancellationToken.None, null!); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Created }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Created }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Created }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + { + var createModel = new CreateFolderRequestModel + { + Name = Guid.NewGuid().ToString(), + Parent = null, + Id = Guid.NewGuid(), + }; + + return await Client.PostAsync(Url, JsonContent.Create(createModel)); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/DeleteElementFolderControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/DeleteElementFolderControllerTests.cs new file mode 100644 index 000000000000..50a96530b7b2 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/DeleteElementFolderControllerTests.cs @@ -0,0 +1,46 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.Folder; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.Folder; + +public class DeleteElementFolderControllerTests : ManagementApiUserGroupTestBase +{ + private IElementContainerService ElementContainerService => GetRequiredService(); + + private Guid _folderKey; + + [SetUp] + public async Task Setup() + { + var result = await ElementContainerService.CreateAsync(null, Guid.NewGuid().ToString(), null, Constants.Security.SuperUserKey); + _folderKey = result.Result!.Key; + } + + protected override Expression> MethodSelector => + x => x.Delete(CancellationToken.None, _folderKey); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + => await Client.DeleteAsync(Url); +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/MoveElementFolderControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/MoveElementFolderControllerTests.cs new file mode 100644 index 000000000000..1dbebe90e3da --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/MoveElementFolderControllerTests.cs @@ -0,0 +1,60 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.Folder; +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Folder; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.Folder; + +public class MoveElementFolderControllerTests : ManagementApiUserGroupTestBase +{ + private IElementContainerService ElementContainerService => GetRequiredService(); + + private Guid _folderKey; + private Guid _targetFolderKey; + + [SetUp] + public async Task Setup() + { + var folderResult = await ElementContainerService.CreateAsync(null, Guid.NewGuid().ToString(), null, Constants.Security.SuperUserKey); + _folderKey = folderResult.Result!.Key; + + var targetResult = await ElementContainerService.CreateAsync(null, Guid.NewGuid().ToString(), null, Constants.Security.SuperUserKey); + _targetFolderKey = targetResult.Result!.Key; + } + + protected override Expression> MethodSelector => + x => x.Move(CancellationToken.None, _folderKey, null!); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + { + var moveModel = new MoveFolderRequestModel + { + Target = new ReferenceByIdModel(_targetFolderKey), + }; + + return await Client.PutAsync(Url, JsonContent.Create(moveModel)); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/MoveToRecycleBinElementFolderControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/MoveToRecycleBinElementFolderControllerTests.cs new file mode 100644 index 000000000000..fefbad737e7a --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/MoveToRecycleBinElementFolderControllerTests.cs @@ -0,0 +1,46 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.Folder; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.Folder; + +public class MoveToRecycleBinElementFolderControllerTests : ManagementApiUserGroupTestBase +{ + private IElementContainerService ElementContainerService => GetRequiredService(); + + private Guid _folderKey; + + [SetUp] + public async Task Setup() + { + var result = await ElementContainerService.CreateAsync(null, Guid.NewGuid().ToString(), null, Constants.Security.SuperUserKey); + _folderKey = result.Result!.Key; + } + + protected override Expression> MethodSelector => + x => x.Move(CancellationToken.None, _folderKey); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + => await Client.PutAsync(Url, null); +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/UpdateElementFolderControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/UpdateElementFolderControllerTests.cs new file mode 100644 index 000000000000..2c075ef7028f --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Folder/UpdateElementFolderControllerTests.cs @@ -0,0 +1,55 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.Folder; +using Umbraco.Cms.Api.Management.ViewModels.Folder; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.Folder; + +public class UpdateElementFolderControllerTests : ManagementApiUserGroupTestBase +{ + private IElementContainerService ElementContainerService => GetRequiredService(); + + private Guid _folderKey; + + [SetUp] + public async Task Setup() + { + var result = await ElementContainerService.CreateAsync(null, Guid.NewGuid().ToString(), null, Constants.Security.SuperUserKey); + _folderKey = result.Result!.Key; + } + + protected override Expression> MethodSelector => + x => x.Update(CancellationToken.None, _folderKey, null!); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + { + var updateModel = new UpdateFolderResponseModel + { + Name = Guid.NewGuid().ToString(), + }; + + return await Client.PutAsync(Url, JsonContent.Create(updateModel)); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/Item/ItemElementItemControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Item/ItemElementItemControllerTests.cs new file mode 100644 index 000000000000..8282d58dca29 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Item/ItemElementItemControllerTests.cs @@ -0,0 +1,64 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.Item; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.Item; + +public class ItemElementItemControllerTests : ManagementApiUserGroupTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementKey; + + [SetUp] + public async Task Setup() + { + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = Guid.NewGuid().ToString() }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + } + + protected override Expression> MethodSelector => + x => x.Item(CancellationToken.None, new HashSet { _elementKey }); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + => await Client.GetAsync($"{Url}?id={_elementKey}"); +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/MoveElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/MoveElementControllerTests.cs new file mode 100644 index 000000000000..1c72425f28ec --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/MoveElementControllerTests.cs @@ -0,0 +1,83 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element; +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Element; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class MoveElementControllerTests : ManagementApiUserGroupTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IElementContainerService ElementContainerService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementKey; + private Guid _targetContainerKey; + + [SetUp] + public async Task Setup() + { + // Create element type + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + // Create element to move + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = "Element to Move" }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + + // Create target container + var targetResult = await ElementContainerService.CreateAsync(null, Guid.NewGuid().ToString(), null, Constants.Security.SuperUserKey); + _targetContainerKey = targetResult.Result!.Key; + } + + protected override Expression> MethodSelector => + x => x.Move(CancellationToken.None, _elementKey, null!); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + { + var moveElementRequestModel = new MoveElementRequestModel + { + Target = new ReferenceByIdModel(_targetContainerKey), + }; + + return await Client.PutAsync(Url, JsonContent.Create(moveElementRequestModel)); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/MoveToRecycleBinElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/MoveToRecycleBinElementControllerTests.cs new file mode 100644 index 000000000000..5a2b746834a9 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/MoveToRecycleBinElementControllerTests.cs @@ -0,0 +1,66 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class MoveToRecycleBinElementControllerTests : ManagementApiUserGroupTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementTypeKey; + private Guid _elementKey; + + [SetUp] + public async Task Setup() + { + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + _elementTypeKey = elementType.Key; + + var createModel = new ElementCreateModel + { + ContentTypeKey = _elementTypeKey, + ParentKey = null, + Variants = [new VariantModel { Name = Guid.NewGuid().ToString() }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + } + + protected override Expression> MethodSelector => + x => x.MoveToRecycleBin(CancellationToken.None, _elementKey); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + => await Client.PutAsync(Url, null); +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/PublishElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/PublishElementControllerTests.cs new file mode 100644 index 000000000000..8d3d6bc0bf0d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/PublishElementControllerTests.cs @@ -0,0 +1,74 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Api.Management.ViewModels.Element; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class PublishElementControllerTests : ManagementApiUserGroupTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementKey; + + [SetUp] + public async Task Setup() + { + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = "Test Element" }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + } + + protected override Expression> MethodSelector => + x => x.Publish(CancellationToken.None, _elementKey, null!); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + { + var publishModel = new PublishElementRequestModel + { + PublishSchedules = [new CultureAndScheduleRequestModel { Culture = null }], + }; + + return await Client.PutAsync(Url, JsonContent.Create(publishModel)); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/ChildrenElementRecycleBinControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/ChildrenElementRecycleBinControllerTests.cs new file mode 100644 index 000000000000..2909cd94c02c --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/ChildrenElementRecycleBinControllerTests.cs @@ -0,0 +1,71 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.RecycleBin; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.RecycleBin; + +public class ChildrenElementRecycleBinControllerTests : ElementRecycleBinControllerTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IElementContainerService ElementContainerService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _folderKey; + + [SetUp] + public async Task Setup() + { + // Folder + var folderResult = await ElementContainerService.CreateAsync(null, Guid.NewGuid().ToString(), null, Constants.Security.SuperUserKey); + _folderKey = folderResult.Result!.Key; + + // Element Type + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + // Element inside folder + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = _folderKey, + Variants = [new VariantModel { Name = Guid.NewGuid().ToString() }], + }; + await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + + // Move folder to recycle bin (this will move the element inside it too) + await ElementContainerService.MoveToRecycleBinAsync(_folderKey, Constants.Security.SuperUserKey); + } + + protected override Expression> MethodSelector => + x => x.Children(CancellationToken.None, _folderKey, 0, 100); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/DeleteElementFolderRecycleBinControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/DeleteElementFolderRecycleBinControllerTests.cs new file mode 100644 index 000000000000..8a2309df374f --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/DeleteElementFolderRecycleBinControllerTests.cs @@ -0,0 +1,48 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.RecycleBin; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.RecycleBin; + +public class DeleteElementFolderRecycleBinControllerTests : ElementRecycleBinControllerTestBase +{ + private IElementContainerService ElementContainerService => GetRequiredService(); + + private Guid _folderKey; + + [SetUp] + public async Task Setup() + { + var result = await ElementContainerService.CreateAsync(null, Guid.NewGuid().ToString(), null, Constants.Security.SuperUserKey); + _folderKey = result.Result!.Key; + + await ElementContainerService.MoveToRecycleBinAsync(_folderKey, Constants.Security.SuperUserKey); + } + + protected override Expression> MethodSelector => + x => x.Delete(CancellationToken.None, _folderKey); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + => await Client.DeleteAsync(Url); +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/DeleteElementRecycleBinControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/DeleteElementRecycleBinControllerTests.cs new file mode 100644 index 000000000000..52fc0986f3dc --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/DeleteElementRecycleBinControllerTests.cs @@ -0,0 +1,66 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.RecycleBin; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.RecycleBin; + +public class DeleteElementRecycleBinControllerTests : ElementRecycleBinControllerTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementKey; + + [SetUp] + public async Task Setup() + { + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = Guid.NewGuid().ToString() }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + + await ElementEditingService.MoveToRecycleBinAsync(_elementKey, Constants.Security.SuperUserKey); + } + + protected override Expression> MethodSelector => + x => x.Delete(CancellationToken.None, _elementKey); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + => await Client.DeleteAsync(Url); +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/ElementRecycleBinControllerTestBase.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/ElementRecycleBinControllerTestBase.cs new file mode 100644 index 000000000000..ca00179e8978 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/ElementRecycleBinControllerTestBase.cs @@ -0,0 +1,49 @@ +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.RecycleBin; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.RecycleBin; + +public abstract class ElementRecycleBinControllerTestBase : ManagementApiUserGroupTestBase + where T : ElementRecycleBinControllerBase +{ + private IElementContainerService ElementContainerService => GetRequiredService(); + + private IUserGroupService UserGroupService => GetRequiredService(); + + [Test] + public async Task User_With_Non_Root_Element_Start_Node_Cannot_Access_Recycle_Bin() + { + // Create an element container (folder) to use as a non-root start node + var containerResult = await ElementContainerService.CreateAsync( + null, + $"Test Folder {Guid.NewGuid()}", + null, // at root + Constants.Security.SuperUserKey); + Assert.IsTrue(containerResult.Success); + var container = containerResult.Result!; + + // Create a user group with Library section access but with a non-root element start node + var userGroup = new UserGroupBuilder() + .WithAlias(Guid.NewGuid().ToString("N")) + .WithName("Test Group With Element Start Node") + .WithAllowedSections(["library"]) + .WithStartElementId(container.Id) + .Build(); + + await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + + // Authenticate as a user in the group with non-root element start node + await AuthenticateClientAsync(Client, $"startnodetest{Guid.NewGuid():N}@umbraco.com", "1234567890", userGroup.Key); + + // Try to access the recycle bin + var response = await ClientRequest(); + + // Should be forbidden because user doesn't have root element access + Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode, await response.Content.ReadAsStringAsync()); + } +} \ No newline at end of file diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/EmptyElementRecycleBinControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/EmptyElementRecycleBinControllerTests.cs new file mode 100644 index 000000000000..624e64423e05 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/EmptyElementRecycleBinControllerTests.cs @@ -0,0 +1,64 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.RecycleBin; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.RecycleBin; + +public class EmptyElementRecycleBinControllerTests : ElementRecycleBinControllerTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + [SetUp] + public async Task Setup() + { + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = Guid.NewGuid().ToString() }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + var elementKey = response.Result!.Content!.Key; + + await ElementEditingService.MoveToRecycleBinAsync(elementKey, Constants.Security.SuperUserKey); + } + + protected override Expression> MethodSelector => + x => x.EmptyRecycleBin(CancellationToken.None); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + => await Client.DeleteAsync(Url); +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/RootElementRecycleBinControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/RootElementRecycleBinControllerTests.cs new file mode 100644 index 000000000000..8c6b3a967a0b --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/RootElementRecycleBinControllerTests.cs @@ -0,0 +1,63 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.RecycleBin; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.RecycleBin; + +public class RootElementRecycleBinControllerTests : ElementRecycleBinControllerTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementKey; + + [SetUp] + public async Task Setup() + { + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = Guid.NewGuid().ToString() }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + + await ElementEditingService.MoveToRecycleBinAsync(_elementKey, Constants.Security.SuperUserKey); + } + + protected override Expression> MethodSelector => + x => x.Root(CancellationToken.None, 0, 100); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/SiblingsElementRecycleBinControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/SiblingsElementRecycleBinControllerTests.cs new file mode 100644 index 000000000000..ae79202c58f6 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/RecycleBin/SiblingsElementRecycleBinControllerTests.cs @@ -0,0 +1,75 @@ +using System.Linq.Expressions; +using System.Net; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.RecycleBin; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.RecycleBin; + +public class SiblingsElementRecycleBinControllerTests : ElementRecycleBinControllerTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementKey; + + [SetUp] + public async Task Setup() + { + // Element Type + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + // Create two elements at root so we have siblings in the recycle bin + var createModel1 = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = Guid.NewGuid().ToString() }], + }; + var response1 = await ElementEditingService.CreateAsync(createModel1, Constants.Security.SuperUserKey); + _elementKey = response1.Result!.Content!.Key; + + var createModel2 = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = Guid.NewGuid().ToString() }], + }; + var response2 = await ElementEditingService.CreateAsync(createModel2, Constants.Security.SuperUserKey); + + // Move both to recycle bin + await ElementEditingService.MoveToRecycleBinAsync(_elementKey, Constants.Security.SuperUserKey); + await ElementEditingService.MoveToRecycleBinAsync(response2.Result!.Content!.Key, Constants.Security.SuperUserKey); + } + + protected override Expression> MethodSelector => + x => x.Siblings(CancellationToken.None, _elementKey, 0, 10, null); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/AncestorsElementTreeControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/AncestorsElementTreeControllerTests.cs new file mode 100644 index 000000000000..174651f88927 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/AncestorsElementTreeControllerTests.cs @@ -0,0 +1,90 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element.Tree; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.Tree; + +public class AncestorsElementTreeControllerTests : ManagementApiUserGroupTestBase +{ + private IElementContainerService ElementContainerService => GetRequiredService(); + + private IUserGroupService UserGroupService => GetRequiredService(); + + private Guid _grandparentKey; + private Guid _parentKey; + private int _parentId; + + [SetUp] + public async Task Setup() + { + // Create grandparent container + var grandparentResult = await ElementContainerService.CreateAsync(null, $"GrandparentContainer {Guid.NewGuid()}", null, Constants.Security.SuperUserKey); + Assert.IsTrue(grandparentResult.Success, $"Failed to create grandparent: {grandparentResult.Status}"); + _grandparentKey = grandparentResult.Result!.Key; + + // Create parent container + var parentResult = await ElementContainerService.CreateAsync(null, $"ParentContainer {Guid.NewGuid()}", _grandparentKey, Constants.Security.SuperUserKey); + Assert.IsTrue(parentResult.Success, $"Failed to create parent: {parentResult.Status}"); + _parentKey = parentResult.Result!.Key; + _parentId = parentResult.Result!.Id; + } + + protected override Expression> MethodSelector => + x => x.Ancestors(CancellationToken.None, _parentKey); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + [Test] + public async Task User_With_Start_Node_Can_Access_Ancestors() + { + // Create a user group with Library section access and start node = parent folder + var userGroup = new UserGroupBuilder() + .WithAlias(Guid.NewGuid().ToString("N")) + .WithName("Test Group With Element Start Node") + .WithAllowedSections(["library"]) + .WithStartElementId(_parentId) + .Build(); + + await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + + // Authenticate as a user in the group with the restricted start node + await AuthenticateClientAsync(Client, $"startnodetest{Guid.NewGuid():N}@umbraco.com", "1234567890", userGroup.Key); + + // Get ancestors of the parent folder (user's start node) + var response = await ClientRequest(); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, await response.Content.ReadAsStringAsync()); + + var result = await response.Content.ReadFromJsonAsync>(JsonSerializerOptions); + Assert.IsNotNull(result); + var ancestors = result.ToList(); + + // Ancestors should include grandparent and parent (the target itself) + Assert.AreEqual(2, ancestors.Count, "Should return grandparent and parent"); + Assert.AreEqual(_grandparentKey, ancestors[0].Id, "First ancestor should be grandparent"); + Assert.AreEqual(_parentKey, ancestors[1].Id, "Second ancestor should be parent (target)"); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/ChildrenElementTreeControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/ChildrenElementTreeControllerTests.cs new file mode 100644 index 000000000000..d044a8a78027 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/ChildrenElementTreeControllerTests.cs @@ -0,0 +1,116 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Controllers.Element.Tree; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.Tree; + +public class ChildrenElementTreeControllerTests : ManagementApiUserGroupTestBase +{ + private IElementContainerService ElementContainerService => GetRequiredService(); + + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IUserGroupService UserGroupService => GetRequiredService(); + + private Guid _parentKey; + + [SetUp] + public async Task Setup() + { + // Create element type + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName($"Test Element {Guid.NewGuid()}") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + // Create parent container + var parentResult = await ElementContainerService.CreateAsync(null, $"ParentContainer {Guid.NewGuid()}", null, Constants.Security.SuperUserKey); + Assert.IsTrue(parentResult.Success, $"Failed to create parent: {parentResult.Status}"); + _parentKey = parentResult.Result!.Key; + + // Create child container + var childContainerResult = await ElementContainerService.CreateAsync(null, $"ChildContainer {Guid.NewGuid()}", _parentKey, Constants.Security.SuperUserKey); + Assert.IsTrue(childContainerResult.Success, $"Failed to create child container: {childContainerResult.Status}"); + + // Create child element + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = _parentKey, + Variants = [new VariantModel { Name = $"ChildElement {Guid.NewGuid()}" }], + }; + await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + } + + protected override Expression> MethodSelector => + x => x.Children(CancellationToken.None, _parentKey, 0, 100, false); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + [Test] + public async Task User_With_Start_Node_Cannot_Access_Children_Outside_Start_Node() + { + // Create another folder to be the user's start node (different from the one in Setup) + var startNodeResult = await ElementContainerService.CreateAsync( + null, + $"Start Node Folder {Guid.NewGuid()}", + _parentKey, + Constants.Security.SuperUserKey); + Assert.IsTrue(startNodeResult.Success, $"Failed to create start node folder: {startNodeResult.Status}"); + var startNodeFolder = startNodeResult.Result!; + + // Create a user group with Library section access but with a non-root element start node + var userGroup = new UserGroupBuilder() + .WithAlias(Guid.NewGuid().ToString("N")) + .WithName("Test Group With Element Start Node") + .WithAllowedSections(["library"]) + .WithStartElementId(startNodeFolder.Id) + .Build(); + + await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + + // Authenticate as a user in the group with the restricted start node + await AuthenticateClientAsync(Client, $"startnodetest{Guid.NewGuid():N}@umbraco.com", "1234567890", userGroup.Key); + + // Try to access the children of the parent folder created in Setup (which is outside the start node) + var response = await ClientRequest(); + + // Should succeed + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, await response.Content.ReadAsStringAsync()); + + // Parse response and verify only folder1 is returned + var result = await response.Content.ReadFromJsonAsync>(JsonSerializerOptions); + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Total); + Assert.AreEqual(startNodeFolder.Key, result.Items.First().Id); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/RootElementTreeControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/RootElementTreeControllerTests.cs new file mode 100644 index 000000000000..a25a990faa90 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/RootElementTreeControllerTests.cs @@ -0,0 +1,87 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Controllers.Element.Tree; +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.Tree; + +public class RootElementTreeControllerTests : ManagementApiUserGroupTestBase +{ + private IElementContainerService ElementContainerService => GetRequiredService(); + + private IUserGroupService UserGroupService => GetRequiredService(); + + protected override Expression> MethodSelector => + x => x.Root(CancellationToken.None, 0, 100, false); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + [Test] + public async Task User_With_Start_Node_Only_Sees_Permitted_Roots() + { + // Create two folders at root level + var folder1Result = await ElementContainerService.CreateAsync( + null, + $"Folder 1 {Guid.NewGuid()}", + null, + Constants.Security.SuperUserKey); + Assert.IsTrue(folder1Result.Success); + var folder1 = folder1Result.Result!; + + var folder2Result = await ElementContainerService.CreateAsync( + null, + $"Folder 2 {Guid.NewGuid()}", + null, + Constants.Security.SuperUserKey); + Assert.IsTrue(folder2Result.Success); + + // Create a user group with start node = folder1 only + var userGroup = new UserGroupBuilder() + .WithAlias(Guid.NewGuid().ToString("N")) + .WithName("Test Group With Element Start Node") + .WithAllowedSections(["library"]) + .WithStartElementId(folder1.Id) + .Build(); + + await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + + // Authenticate as a user in that group + await AuthenticateClientAsync(Client, $"startnodetest{Guid.NewGuid():N}@umbraco.com", "1234567890", userGroup.Key); + + // Get root tree items + var response = await ClientRequest(); + + // Should succeed + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, await response.Content.ReadAsStringAsync()); + + // Parse response and verify only folder1 is returned + var result = await response.Content.ReadFromJsonAsync>(JsonSerializerOptions); + Assert.IsNotNull(result); + Assert.AreEqual(1, result.Total); + Assert.AreEqual(folder1.Key, result.Items.First().Id); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/SiblingsElementTreeControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/SiblingsElementTreeControllerTests.cs new file mode 100644 index 000000000000..2a45ed7e805c --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/Tree/SiblingsElementTreeControllerTests.cs @@ -0,0 +1,90 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Controllers.Element.Tree; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element.Tree; + +public class SiblingsElementTreeControllerTests : ManagementApiUserGroupTestBase +{ + private IElementContainerService ElementContainerService => GetRequiredService(); + + private IUserGroupService UserGroupService => GetRequiredService(); + + private Guid _folder1Key; + private int _folder1Id; + + [SetUp] + public async Task Setup() + { + // Create two folders at root level (siblings of each other) + var folder1Result = await ElementContainerService.CreateAsync(null, $"Folder1 {Guid.NewGuid()}", null, Constants.Security.SuperUserKey); + Assert.IsTrue(folder1Result.Success, $"Failed to create folder1: {folder1Result.Status}"); + _folder1Key = folder1Result.Result!.Key; + _folder1Id = folder1Result.Result!.Id; + + var folder2Result = await ElementContainerService.CreateAsync(null, $"Folder2 {Guid.NewGuid()}", null, Constants.Security.SuperUserKey); + Assert.IsTrue(folder2Result.Success, $"Failed to create folder2: {folder2Result.Status}"); + } + + protected override Expression> MethodSelector => + x => x.Siblings(CancellationToken.None, _folder1Key, 10, 10, false); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + [Test] + public async Task User_With_Start_Node_Only_Sees_Accessible_Siblings() + { + // User's start node is folder1, so they have no access to folder2 (its sibling) + var userGroup = new UserGroupBuilder() + .WithAlias(Guid.NewGuid().ToString("N")) + .WithName("Test Group With Element Start Node") + .WithAllowedSections(["library"]) + .WithStartElementId(_folder1Id) + .Build(); + + await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + + await AuthenticateClientAsync(Client, $"startnodetest{Guid.NewGuid():N}@umbraco.com", "1234567890", userGroup.Key); + + // Get siblings of folder1 (folder2 is a sibling but user has no access) + var response = await ClientRequest(); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, await response.Content.ReadAsStringAsync()); + + var result = await response.Content.ReadFromJsonAsync>(JsonSerializerOptions); + Assert.IsNotNull(result); + + // Only folder1 (the target) should be returned; folder2 should be filtered out completely + Assert.AreEqual(1, result.Items.Count(), "Only the target folder should be returned"); + Assert.AreEqual(_folder1Key, result.Items.First().Id, "The target folder should be folder1"); + + // No accessible siblings before or after the target + Assert.AreEqual(0, result.TotalBefore, "No accessible siblings before"); + Assert.AreEqual(0, result.TotalAfter, "No accessible siblings after"); + } +} + diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/UnpublishElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/UnpublishElementControllerTests.cs new file mode 100644 index 000000000000..5871c07e1d37 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/UnpublishElementControllerTests.cs @@ -0,0 +1,79 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Api.Management.ViewModels.Element; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class UnpublishElementControllerTests : ManagementApiUserGroupTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IElementPublishingService ElementPublishingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementKey; + + [SetUp] + public async Task Setup() + { + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = "Test Element" }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + + // Publish the element so we can unpublish it + await ElementPublishingService.PublishAsync(_elementKey, [], Constants.Security.SuperUserKey); + } + + protected override Expression> MethodSelector => + x => x.Unpublish(CancellationToken.None, _elementKey, null!); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + { + var unpublishModel = new UnpublishElementRequestModel + { + Cultures = null, + }; + + return await Client.PutAsync(Url, JsonContent.Create(unpublishModel)); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/UpdateElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/UpdateElementControllerTests.cs new file mode 100644 index 000000000000..59437482662f --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/UpdateElementControllerTests.cs @@ -0,0 +1,74 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element; +using Umbraco.Cms.Api.Management.ViewModels.Element; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class UpdateElementControllerTests : ManagementApiUserGroupTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementKey; + + [SetUp] + public async Task Setup() + { + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = "Test Element" }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + } + + protected override Expression> MethodSelector => + x => x.Update(CancellationToken.None, _elementKey, null!); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + { + var updateModel = new UpdateElementRequestModel + { + Values = [], + Variants = [new ElementVariantRequestModel { Culture = null, Segment = null, Name = "Updated Element" }], + }; + + return await Client.PutAsync(Url, JsonContent.Create(updateModel)); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/ValidateCreateElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/ValidateCreateElementControllerTests.cs new file mode 100644 index 000000000000..41e473a8e531 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/ValidateCreateElementControllerTests.cs @@ -0,0 +1,67 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element; +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.Element; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class ValidateCreateElementControllerTests : ManagementApiUserGroupTestBase +{ + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementTypeKey; + + [SetUp] + public async Task Setup() + { + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + _elementTypeKey = elementType.Key; + } + + protected override Expression> MethodSelector => + x => x.Validate(CancellationToken.None, null!); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + { + var createModel = new CreateElementRequestModel + { + DocumentType = new ReferenceByIdModel(_elementTypeKey), + Parent = null, + Id = Guid.NewGuid(), + Values = [], + Variants = [new ElementVariantRequestModel { Culture = null, Segment = null, Name = "Test Element" }], + }; + + return await Client.PostAsync(Url, JsonContent.Create(createModel)); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Element/ValidateUpdateElementControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Element/ValidateUpdateElementControllerTests.cs new file mode 100644 index 000000000000..75bee4c2d711 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Element/ValidateUpdateElementControllerTests.cs @@ -0,0 +1,75 @@ +using System.Linq.Expressions; +using System.Net; +using System.Net.Http.Json; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Element; +using Umbraco.Cms.Api.Management.ViewModels.Element; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Element; + +public class ValidateUpdateElementControllerTests : ManagementApiUserGroupTestBase +{ + private IElementEditingService ElementEditingService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private Guid _elementKey; + + [SetUp] + public async Task Setup() + { + var elementType = new ContentTypeBuilder() + .WithAlias(Guid.NewGuid().ToString()) + .WithName("Test Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + + var createModel = new ElementCreateModel + { + ContentTypeKey = elementType.Key, + ParentKey = null, + Variants = [new VariantModel { Name = "Test Element" }], + }; + var response = await ElementEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + _elementKey = response.Result!.Content!.Key; + } + + protected override Expression> MethodSelector => + x => x.Validate(CancellationToken.None, _elementKey, null!); + + protected override UserGroupAssertionModel AdminUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel EditorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel SensitiveDataUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel TranslatorUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Forbidden }; + + protected override UserGroupAssertionModel WriterUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.OK }; + + protected override UserGroupAssertionModel UnauthorizedUserGroupAssertionModel + => new() { ExpectedStatusCode = HttpStatusCode.Unauthorized }; + + protected override async Task ClientRequest() + { + var validateModel = new ValidateUpdateElementRequestModel + { + Values = [], + Variants = [new ElementVariantRequestModel { Culture = null, Segment = null, Name = "Updated Element" }], + Cultures = null, + }; + + return await Client.PutAsync(Url, JsonContent.Create(validateModel)); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Factories/UserPresentationFactoryTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Factories/UserPresentationFactoryTests.cs index f9d103f1e44b..9c1a9e5f8ce3 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Factories/UserPresentationFactoryTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Factories/UserPresentationFactoryTests.cs @@ -50,7 +50,6 @@ protected override void ConfigureTestServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); - } [Test] @@ -99,6 +98,8 @@ public async Task Can_Create_Current_User_Response_Model() Assert.IsFalse(model.HasMediaRootAccess); Assert.AreEqual(1, model.MediaStartNodeIds.Count); Assert.AreEqual(rootMediaFolder.Key, model.MediaStartNodeIds.First().Id); + Assert.IsTrue(model.HasElementRootAccess); + Assert.AreEqual(0, model.ElementStartNodeIds.Count); Assert.IsFalse(model.HasAccessToSensitiveData); } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs b/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs index 598cfc015056..b9682e1693e6 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs @@ -5,9 +5,12 @@ using System.Net.Mime; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Text.Json.Serialization; using System.Web; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NUnit.Framework; using OpenIddict.Abstractions; using Umbraco.Cms.Api.Management.Controllers; @@ -32,10 +35,20 @@ namespace Umbraco.Cms.Tests.Integration.ManagementApi; public abstract class ManagementApiTest : UmbracoTestServerTestBase where T : ManagementApiControllerBase { - private static readonly Dictionary _tokenCache = new(); private static readonly SHA256 _sha256 = SHA256.Create(); + protected JsonSerializerOptions JsonSerializerOptions + { + get + { + var options = GetRequiredService>(); + return options + .Get(Constants.JsonOptionsNames.BackOffice) + .JsonSerializerOptions; + } + } + protected abstract Expression> MethodSelector { get; set; } protected string Url => GetManagementApiUrl(MethodSelector); diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.ChildUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.ChildUserAccessEntities.cs new file mode 100644 index 000000000000..5683c2947647 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.ChildUserAccessEntities.cs @@ -0,0 +1,257 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; + +public partial class UserStartNodeEntitiesServiceElementTests +{ + [Test] + public async Task ChildUserAccessEntities_FirstAndLastChildOfRoot_YieldsBothInFirstPage_AsAllowed() + { + // Child containers "C1-C1" and "C1-C10" are used as start nodes + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C1-C1"].Id, ItemsByName["C1-C10"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C1"].Key, + 0, + 3, + BySortOrder, + out var totalItems) + .ToArray(); + + // expected total is 2, because only two items under "C1" are allowed (note the page size is 3 for good measure) + Assert.AreEqual(2, totalItems); + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + // first and last child containers are the ones allowed + Assert.AreEqual(ItemsByName["C1-C1"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["C1-C10"].Key, children[1].Entity.Key); + + // both are allowed (they are the actual start nodes) + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_InAndOutOfScope_YieldsOnlyChildrenInScope() + { + // Child containers "C1-C5" and "C2-C10" are used as start nodes + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C1-C5"].Id, ItemsByName["C2-C10"].Id); + Assert.AreEqual(2, elementStartNodePaths.Length); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C2"].Key, + 0, + 10, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(1, totalItems); + Assert.AreEqual(1, children.Length); + Assert.Multiple(() => + { + // only the "C2-C10" container is returned, as "C1-C5" is out of scope + Assert.AreEqual(ItemsByName["C2-C10"].Key, children[0].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_OutOfScope_YieldsNothing() + { + // Child containers "C1-C5" and "C2-C10" are used as start nodes + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C1-C5"].Id, ItemsByName["C2-C10"].Id); + Assert.AreEqual(2, elementStartNodePaths.Length); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C3"].Key, + 0, + 10, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(0, totalItems); + Assert.AreEqual(0, children.Length); + } + + [Test] + public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginate() + { + // Child containers used as start nodes + var elementStartNodePaths = await CreateUserAndGetStartNodePaths( + ItemsByName["C1-C1"].Id, + ItemsByName["C1-C3"].Id, + ItemsByName["C1-C5"].Id, + ItemsByName["C1-C7"].Id, + ItemsByName["C1-C9"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C1"].Key, + 0, + 2, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + // page size is 2 + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(ItemsByName["C1-C1"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["C1-C3"].Key, children[1].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + + // next result page + children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C1"].Key, + 2, + 2, + BySortOrder, + out totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + // page size is still 2 + Assert.AreEqual(2, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(ItemsByName["C1-C5"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["C1-C7"].Key, children[1].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + Assert.IsTrue(children[1].HasAccess); + }); + + // next result page + children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C1"].Key, + 4, + 2, + BySortOrder, + out totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + // page size is still 2, but this is the last result page + Assert.AreEqual(1, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(ItemsByName["C1-C9"].Key, children[0].Entity.Key); + Assert.IsTrue(children[0].HasAccess); + }); + } + + [Test] + public async Task ChildUserAccessEntities_ChildContainerAsStartNode_YieldsAllGrandchildren_AsAllowed() + { + // Child container "C3-C3" is used as start node + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C3-C3"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C3-C3"].Key, + 0, + 100, + BySortOrder, + out var totalItems) + .ToArray(); + + Assert.AreEqual(5, totalItems); + Assert.AreEqual(5, children.Length); + Assert.Multiple(() => + { + // all children of "C3-C3" (which are elements) should be allowed because "C3-C3" is a start node + foreach (var childNumber in Enumerable.Range(1, 5)) + { + var child = children[childNumber - 1]; + Assert.AreEqual(ItemsByName[$"C3-C3-E{childNumber}"].Key, child.Entity.Key); + Assert.IsTrue(child.HasAccess); + } + }); + } + + [Test] + public async Task ChildUserAccessEntities_ReverseStartNodeOrder_DoesNotAffectResultOrder() + { + // Child containers "C3-C3", "C3-C2", "C3-C1" are used as start nodes (in reverse order) + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C3-C3"].Id, ItemsByName["C3-C2"].Id, ItemsByName["C3-C1"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C3"].Key, + 0, + 10, + BySortOrder, + out var totalItems) + .ToArray(); + Assert.AreEqual(3, totalItems); + Assert.AreEqual(3, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(ItemsByName["C3-C1"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["C3-C2"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["C3-C3"].Key, children[2].Entity.Key); + }); + } + + [Test] + public async Task ChildUserAccessEntities_RootContainerAsStartNode_YieldsMixedChildrenContainersAndElements_AsAllowed() + { + // Root container "C1" is used as start node - its children include both child containers and elements + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C1"].Id); + + var children = UserStartNodeEntitiesService + .ChildUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C1"].Key, + 0, + 100, + BySortOrder, + out var totalItems) + .ToArray(); + + // C1 has 10 child containers (C1-C1 through C1-C10) and 2 elements (C1-E1, C1-E2) + Assert.AreEqual(12, totalItems); + Assert.AreEqual(12, children.Length); + Assert.Multiple(() => + { + // All children should be allowed because "C1" is a start node + Assert.IsTrue(children.All(c => c.HasAccess)); + + // Verify we have both containers and elements in the results + var containerChildren = children.Where(c => c.Entity.Name!.Contains("-C")).ToArray(); + var elementChildren = children.Where(c => c.Entity.Name!.Contains("-E")).ToArray(); + Assert.AreEqual(10, containerChildren.Length); + Assert.AreEqual(2, elementChildren.Length); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.RootUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.RootUserAccessEntities.cs new file mode 100644 index 000000000000..60c353ef7682 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.RootUserAccessEntities.cs @@ -0,0 +1,58 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; + +public partial class UserStartNodeEntitiesServiceElementTests +{ + [Test] + public async Task RootUserAccessEntities_FirstAndLastRootContainer_YieldsBoth_AsAllowed() + { + // Root containers "C1" and "C5" are used as start nodes + var elementStartNodeIds = await CreateUserAndGetStartNodeIds(ItemsByName["C1"].Id, ItemsByName["C5"].Id); + + var roots = UserStartNodeEntitiesService + .RootUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodeIds) + .ToArray(); + + // expected total is 2, because only two items at root ("C1" and "C5") are allowed + Assert.AreEqual(2, roots.Length); + Assert.Multiple(() => + { + // first and last root containers are the ones allowed + Assert.AreEqual(ItemsByName["C1"].Key, roots[0].Entity.Key); + Assert.AreEqual(ItemsByName["C5"].Key, roots[1].Entity.Key); + + // both are allowed (they are the actual start nodes) + Assert.IsTrue(roots[0].HasAccess); + Assert.IsTrue(roots[1].HasAccess); + }); + } + + [Test] + public async Task RootUserAccessEntities_ChildContainersAsStartNode_YieldsChildRoots_AsNotAllowed() + { + // Child containers "C1-C3", "C3-C3", "C5-C3" are used as start nodes + var elementStartNodeIds = await CreateUserAndGetStartNodeIds(ItemsByName["C1-C3"].Id, ItemsByName["C3-C3"].Id, ItemsByName["C5-C3"].Id); + + var roots = UserStartNodeEntitiesService + .RootUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodeIds) + .ToArray(); + + Assert.AreEqual(3, roots.Length); + Assert.Multiple(() => + { + // the three start nodes are the children of the "C1", "C3" and "C5" roots, respectively, so these are expected as roots + Assert.AreEqual(ItemsByName["C1"].Key, roots[0].Entity.Key); + Assert.AreEqual(ItemsByName["C3"].Key, roots[1].Entity.Key); + Assert.AreEqual(ItemsByName["C5"].Key, roots[2].Entity.Key); + + // all are disallowed - only the children (the actual start nodes) are allowed + Assert.IsTrue(roots.All(r => r.HasAccess is false)); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.SiblingUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.SiblingUserAccessEntities.cs new file mode 100644 index 000000000000..be3240263c88 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.SiblingUserAccessEntities.cs @@ -0,0 +1,202 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; + +public partial class UserStartNodeEntitiesServiceElementTests +{ + [Test] + public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParent_YieldsAllContainerSiblings_AsAllowed() + { + // Root container "C1" is used as start node + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C1"].Id); + + var siblings = UserStartNodeEntitiesService + .SiblingUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C1-C5"].Key, + 2, + 2, + BySortOrder, + out long totalBefore, + out long totalAfter) + .ToArray(); + + // C1 has 10 child containers (C1-C1 through C1-C10) and 2 elements (C1-E1, C1-E2) = 12 total + // Target is C1-C5, requesting 2 before and 2 after + // Before C1-C5: C1-C1, C1-C2, C1-C3, C1-C4 = 4 items, returning 2, so totalBefore = 4 - 2 = 2 + // After C1-C5: C1-C6 through C1-C10 (5) + C1-E1, C1-E2 (2) = 7 items, returning 2, so totalAfter = 7 - 2 = 5 + Assert.AreEqual(2, totalBefore); + Assert.AreEqual(5, totalAfter); + Assert.AreEqual(5, siblings.Length); + Assert.Multiple(() => + { + // Siblings returned: C1-C3, C1-C4, C1-C5, C1-C6, C1-C7 + Assert.AreEqual(ItemsByName["C1-C3"].Key, siblings[0].Entity.Key); + Assert.AreEqual(ItemsByName["C1-C4"].Key, siblings[1].Entity.Key); + Assert.AreEqual(ItemsByName["C1-C5"].Key, siblings[2].Entity.Key); + Assert.AreEqual(ItemsByName["C1-C6"].Key, siblings[3].Entity.Key); + Assert.AreEqual(ItemsByName["C1-C7"].Key, siblings[4].Entity.Key); + Assert.IsTrue(siblings.All(s => s.HasAccess)); + }); + } + + [Test] + public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParentAndTarget_YieldsOnlyTarget_AsAllowed() + { + // See notes on ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOnlyGrandchild. + + // Root container "C1" and child container "C1-C5" are used as start nodes + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C1"].Id, ItemsByName["C1-C5"].Id); + + var siblings = UserStartNodeEntitiesService + .SiblingUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C1-C5"].Key, + 2, + 2, + BySortOrder, + out long totalBefore, + out long totalAfter) + .ToArray(); + + Assert.AreEqual(0, totalBefore); + Assert.AreEqual(0, totalAfter); + Assert.AreEqual(1, siblings.Length); + Assert.Multiple(() => + { + Assert.AreEqual(ItemsByName["C1-C5"].Key, siblings[0].Entity.Key); + Assert.IsTrue(siblings[0].HasAccess); + }); + } + + [Test] + public async Task SiblingUserAccessEntities_WithStartNodeOfTarget_YieldsOnlyTarget_AsAllowed() + { + // Child container "C1-C5" is used as start node + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C1-C5"].Id); + + var siblings = UserStartNodeEntitiesService + .SiblingUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C1-C5"].Key, + 2, + 2, + BySortOrder, + out long totalBefore, + out long totalAfter) + .ToArray(); + + Assert.AreEqual(0, totalBefore); + Assert.AreEqual(0, totalAfter); + Assert.AreEqual(1, siblings.Length); + Assert.Multiple(() => + { + Assert.AreEqual(ItemsByName["C1-C5"].Key, siblings[0].Entity.Key); + Assert.IsTrue(siblings[0].HasAccess); + }); + } + + [Test] + public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetAndSiblings_YieldsOnlyPermitted_AsAllowed() + { + // Multiple child containers are used as start nodes + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C1-C3"].Id, ItemsByName["C1-C5"].Id, ItemsByName["C1-C7"].Id, ItemsByName["C1-C10"].Id); + + var siblings = UserStartNodeEntitiesService + .SiblingUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C1-C5"].Key, + 1, + 1, + BySortOrder, + out long totalBefore, + out long totalAfter) + .ToArray(); + + Assert.AreEqual(0, totalBefore); + Assert.AreEqual(1, totalAfter); + Assert.AreEqual(3, siblings.Length); + + Assert.Multiple(() => + { + Assert.AreEqual(ItemsByName["C1-C3"].Key, siblings[0].Entity.Key); + Assert.IsTrue(siblings[0].HasAccess); + Assert.AreEqual(ItemsByName["C1-C5"].Key, siblings[1].Entity.Key); + Assert.IsTrue(siblings[1].HasAccess); + Assert.AreEqual(ItemsByName["C1-C7"].Key, siblings[2].Entity.Key); + Assert.IsTrue(siblings[2].HasAccess); + }); + } + + [Test] + public async Task SiblingUserAccessEntities_WithStartNodeOfTargetDescendant_YieldsTarget_AsNotAllowed() + { + // Child container "C1-C5" is used as start node (descendant of root container "C1") + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C1-C5"].Id); + + // Query for siblings of root container "C1" - the parent of our start node + var siblings = UserStartNodeEntitiesService + .SiblingUserAccessEntities( + UmbracoObjectTypes.ElementContainer, + elementStartNodePaths, + ItemsByName["C1"].Key, + 2, + 2, + BySortOrder, + out long totalBefore, + out long totalAfter) + .ToArray(); + + // User can see "C1" (to navigate to their start node "C1-C5"), but doesn't have direct access + Assert.AreEqual(0, totalBefore); + Assert.AreEqual(0, totalAfter); + Assert.AreEqual(1, siblings.Length); + Assert.Multiple(() => + { + Assert.AreEqual(ItemsByName["C1"].Key, siblings[0].Entity.Key); + Assert.IsFalse(siblings[0].HasAccess); + }); + } + + [Test] + public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParent_YieldsMixedSiblings_AsAllowed() + { + // Root container "C1" is used as start node - its children include both containers and elements + var elementStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["C1"].Id); + + // Query for siblings of element "C1-E1" (which has container siblings too) + var siblings = UserStartNodeEntitiesService + .SiblingUserAccessEntities( + [UmbracoObjectTypes.Element, UmbracoObjectTypes.ElementContainer], + elementStartNodePaths, + ItemsByName["C1-E1"].Key, + 2, + 2, + BySortOrder, + out long totalBefore, + out long totalAfter) + .ToArray(); + + // C1-E1 is after all child containers (C1-C1 through C1-C10), so it has 10 items before it + // and C1-E2 after it + // Before C1-E1: C1-C1 through C1-C10 = 10 items, returning 2, so totalBefore = 10 - 2 = 8 + // After C1-E1: C1-E2 = 1 item, returning 1 (not 2 since only 1 exists), so totalAfter = 0 + Assert.AreEqual(8, totalBefore); + Assert.AreEqual(0, totalAfter); + Assert.AreEqual(4, siblings.Length); // 2 before + target + 1 after + Assert.Multiple(() => + { + // Siblings returned: C1-C9, C1-C10, C1-E1, C1-E2 + Assert.AreEqual(ItemsByName["C1-C9"].Key, siblings[0].Entity.Key); + Assert.AreEqual(ItemsByName["C1-C10"].Key, siblings[1].Entity.Key); + Assert.AreEqual(ItemsByName["C1-E1"].Key, siblings[2].Entity.Key); + Assert.AreEqual(ItemsByName["C1-E2"].Key, siblings[3].Entity.Key); + Assert.IsTrue(siblings.All(s => s.HasAccess)); + }); + } +} \ No newline at end of file diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.cs new file mode 100644 index 000000000000..14882c340e0e --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceElementTests.cs @@ -0,0 +1,114 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; + +/// +/// Element tests use a folder/container structure with elements mixed at each level: +/// - Level 1 (Root): Containers "C1"-"C5" + Elements "E1"-"E3" +/// - Level 2: Child containers "C1-C1" through "C1-C10" + Elements "C1-E1", "C1-E2" under each root container +/// - Level 3: Elements "C1-C1-E1" through "C1-C1-E5" under each child container +/// +[TestFixture] +public partial class UserStartNodeEntitiesServiceElementTests : UserStartNodeEntitiesServiceTestsBase +{ + private IElementService ElementService => GetRequiredService(); + + private IElementContainerService ElementContainerService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + protected override UmbracoObjectTypes ObjectType => UmbracoObjectTypes.Element; + + protected override string SectionAlias => Constants.Applications.Library; + + protected override async Task CreateContentTypeAndHierarchy() + { + // Create the element content type + var contentType = new ContentTypeBuilder() + .WithAlias("theElementType") + .WithIsElement(true) + .Build(); + contentType.AllowedAsRoot = true; + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + + // Create hierarchy with mixed containers and elements at each level + foreach (var rootNumber in Enumerable.Range(1, 5)) + { + // Level 1: Root containers (folders) + var rootContainerResult = await ElementContainerService.CreateAsync( + null, + $"C{rootNumber}", + null, // parent at root + Constants.Security.SuperUserKey); + Assert.IsTrue(rootContainerResult.Success); + Assert.NotNull(rootContainerResult.Result); + var rootContainer = rootContainerResult.Result; + ItemsByName[rootContainer.Name!] = (rootContainer.Id, rootContainer.Key); + + foreach (var childNumber in Enumerable.Range(1, 10)) + { + // Level 2: Child containers (folders) + var childContainerResult = await ElementContainerService.CreateAsync( + null, + $"C{rootNumber}-C{childNumber}", + rootContainer.Key, + Constants.Security.SuperUserKey); + Assert.IsTrue(childContainerResult.Success); + Assert.NotNull(childContainerResult.Result); + var childContainer = childContainerResult.Result; + ItemsByName[childContainer.Name!] = (childContainer.Id, childContainer.Key); + + foreach (var grandChildNumber in Enumerable.Range(1, 5)) + { + // Level 3: Elements (leaf nodes) + var element = ElementService.Create($"C{rootNumber}-C{childNumber}-E{grandChildNumber}", contentType.Alias); + element.ParentId = childContainer.Id; + var saveElementResult = ElementService.Save([element]); + Assert.IsTrue(saveElementResult.Success); + ItemsByName[element.Name!] = (element.Id, element.Key); + } + } + + // Level 2: Elements alongside child containers (mixed level) + foreach (var elementNumber in Enumerable.Range(1, 2)) + { + var element = ElementService.Create($"C{rootNumber}-E{elementNumber}", contentType.Alias); + element.ParentId = rootContainer.Id; + var saveElementResult = ElementService.Save([element]); + Assert.IsTrue(saveElementResult.Success); + ItemsByName[element.Name!] = (element.Id, element.Key); + } + } + + // Level 1: Root elements alongside root containers (mixed level) + foreach (var elementNumber in Enumerable.Range(1, 3)) + { + var element = ElementService.Create($"E{elementNumber}", contentType.Alias); + var saveElementResult = ElementService.Save([element]); + Assert.IsTrue(saveElementResult.Success); + ItemsByName[element.Name!] = (element.Id, element.Key); + } + } + + protected override void ClearUserGroupStartNode(IUserGroup userGroup) + => userGroup.StartElementId = null; + + protected override Core.Models.Membership.User BuildUserWithStartNodes(int[] startNodeIds) + => new UserBuilder() + .WithName(Guid.NewGuid().ToString("N")) + .WithStartElementIds(startNodeIds) + .Build(); + + protected override string[]? GetStartNodePaths(Cms.Core.Models.Membership.User user) + => user.GetElementStartNodePaths(EntityService, AppCaches.NoCache); + + protected override int[]? CalculateStartNodeIds(Cms.Core.Models.Membership.User user) + => user.CalculateElementStartNodeIds(EntityService, AppCaches.NoCache); +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.RootUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.RootUserAccessEntities.cs index a6ab1ff13110..f3ccb9cd5fa1 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.RootUserAccessEntities.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.RootUserAccessEntities.cs @@ -8,7 +8,7 @@ public partial class UserStartNodeEntitiesServiceMediaTests [Test] public async Task RootUserAccessEntities_FirstAndLastRoot_YieldsBoth_AsAllowed() { - var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_mediaByName["1"].Id, _mediaByName["5"].Id); + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(ItemsByName["1"].Id, ItemsByName["5"].Id); var roots = UserStartNodeEntitiesService .RootUserAccessEntities( @@ -21,8 +21,8 @@ public async Task RootUserAccessEntities_FirstAndLastRoot_YieldsBoth_AsAllowed() Assert.Multiple(() => { // first and last content items are the ones allowed - Assert.AreEqual(_mediaByName["1"].Key, roots[0].Entity.Key); - Assert.AreEqual(_mediaByName["5"].Key, roots[1].Entity.Key); + Assert.AreEqual(ItemsByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(ItemsByName["5"].Key, roots[1].Entity.Key); // explicitly verify the entity sort order, both so we know sorting works, // and so we know it's actually the first and last item at root @@ -38,7 +38,7 @@ public async Task RootUserAccessEntities_FirstAndLastRoot_YieldsBoth_AsAllowed() [Test] public async Task RootUserAccessEntities_ChildrenAsStartNode_YieldsChildRoots_AsNotAllowed() { - var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_mediaByName["1-3"].Id, _mediaByName["3-3"].Id, _mediaByName["5-3"].Id); + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(ItemsByName["1-3"].Id, ItemsByName["3-3"].Id, ItemsByName["5-3"].Id); var roots = UserStartNodeEntitiesService .RootUserAccessEntities( @@ -50,9 +50,9 @@ public async Task RootUserAccessEntities_ChildrenAsStartNode_YieldsChildRoots_As Assert.Multiple(() => { // the three start nodes are the children of the "1", "3" and "5" roots, respectively, so these are expected as roots - Assert.AreEqual(_mediaByName["1"].Key, roots[0].Entity.Key); - Assert.AreEqual(_mediaByName["3"].Key, roots[1].Entity.Key); - Assert.AreEqual(_mediaByName["5"].Key, roots[2].Entity.Key); + Assert.AreEqual(ItemsByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(ItemsByName["3"].Key, roots[1].Entity.Key); + Assert.AreEqual(ItemsByName["5"].Key, roots[2].Entity.Key); // all are disallowed - only the children (the actual start nodes) are allowed Assert.IsTrue(roots.All(r => r.HasAccess is false)); @@ -62,7 +62,7 @@ public async Task RootUserAccessEntities_ChildrenAsStartNode_YieldsChildRoots_As [Test] public async Task RootUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchildRoots_AsNotAllowed() { - var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_mediaByName["1-2-3"].Id, _mediaByName["2-3-4"].Id, _mediaByName["3-4-5"].Id); + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(ItemsByName["1-2-3"].Id, ItemsByName["2-3-4"].Id, ItemsByName["3-4-5"].Id); var roots = UserStartNodeEntitiesService .RootUserAccessEntities( @@ -74,9 +74,9 @@ public async Task RootUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchi Assert.Multiple(() => { // the three start nodes are the grandchildren of the "1", "2" and "3" roots, respectively, so these are expected as roots - Assert.AreEqual(_mediaByName["1"].Key, roots[0].Entity.Key); - Assert.AreEqual(_mediaByName["2"].Key, roots[1].Entity.Key); - Assert.AreEqual(_mediaByName["3"].Key, roots[2].Entity.Key); + Assert.AreEqual(ItemsByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(ItemsByName["2"].Key, roots[1].Entity.Key); + Assert.AreEqual(ItemsByName["3"].Key, roots[2].Entity.Key); // all are disallowed - only the grandchildren (the actual start nodes) are allowed Assert.IsTrue(roots.All(r => r.HasAccess is false)); diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.SiblingUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.SiblingUserAccessEntities.cs index d545416eb1ea..cac7cb7a4b5a 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.SiblingUserAccessEntities.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.SiblingUserAccessEntities.cs @@ -8,13 +8,13 @@ public partial class UserStartNodeEntitiesServiceMediaTests [Test] public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParent_YieldsAll_AsAllowed() { - var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1"].Id); + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1"].Id); var siblings = UserStartNodeEntitiesService .SiblingUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["1-5"].Key, + ItemsByName["1-5"].Key, 2, 2, BySortOrder, @@ -29,7 +29,7 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParent_YieldsAl { for (int i = 0; i < 4; i++) { - Assert.AreEqual(_mediaByName[$"1-{i + 3}"].Key, siblings[i].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-{i + 3}"].Key, siblings[i].Entity.Key); Assert.IsTrue(siblings[i].HasAccess); } }); @@ -40,13 +40,13 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParentAndTarget { // See notes on ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOnlyGrandchild. - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1"].Id, _mediaByName["1-5"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1"].Id, ItemsByName["1-5"].Id); var siblings = UserStartNodeEntitiesService .SiblingUserAccessEntities( UmbracoObjectTypes.Media, contentStartNodePaths, - _mediaByName["1-5"].Key, + ItemsByName["1-5"].Key, 2, 2, BySortOrder, @@ -59,7 +59,7 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParentAndTarget Assert.AreEqual(1, siblings.Length); Assert.Multiple(() => { - Assert.AreEqual(_mediaByName[$"1-5"].Key, siblings[0].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-5"].Key, siblings[0].Entity.Key); Assert.IsTrue(siblings[0].HasAccess); }); } @@ -67,13 +67,13 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParentAndTarget [Test] public async Task SiblingUserAccessEntities_WithStartNodeOfTarget_YieldsOnlyTarget_AsAllowed() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-5"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-5"].Id); var siblings = UserStartNodeEntitiesService .SiblingUserAccessEntities( UmbracoObjectTypes.Media, contentStartNodePaths, - _mediaByName["1-5"].Key, + ItemsByName["1-5"].Key, 2, 2, BySortOrder, @@ -86,7 +86,7 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTarget_YieldsOnlyTarg Assert.AreEqual(1, siblings.Length); Assert.Multiple(() => { - Assert.AreEqual(_mediaByName[$"1-5"].Key, siblings[0].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-5"].Key, siblings[0].Entity.Key); Assert.IsTrue(siblings[0].HasAccess); }); } @@ -94,13 +94,13 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTarget_YieldsOnlyTarg [Test] public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetAndSiblings_YieldsOnlyPermitted_AsAllowed() { - var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-3"].Id, _mediaByName["1-5"].Id, _mediaByName["1-7"].Id, _mediaByName["1-10"].Id); + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-3"].Id, ItemsByName["1-5"].Id, ItemsByName["1-7"].Id, ItemsByName["1-10"].Id); var siblings = UserStartNodeEntitiesService .SiblingUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["1-5"].Key, + ItemsByName["1-5"].Key, 1, 1, BySortOrder, @@ -113,11 +113,11 @@ public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetAndSiblings_Y Assert.AreEqual(3, siblings.Length); Assert.Multiple(() => { - Assert.AreEqual(_mediaByName[$"1-3"].Key, siblings[0].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-3"].Key, siblings[0].Entity.Key); Assert.IsTrue(siblings[0].HasAccess); - Assert.AreEqual(_mediaByName[$"1-5"].Key, siblings[1].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-5"].Key, siblings[1].Entity.Key); Assert.IsTrue(siblings[1].HasAccess); - Assert.AreEqual(_mediaByName[$"1-7"].Key, siblings[2].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-7"].Key, siblings[2].Entity.Key); Assert.IsTrue(siblings[2].HasAccess); }); } @@ -125,13 +125,13 @@ public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetAndSiblings_Y [Test] public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetGrandchild_YieldsTarget_AsNotAllowed() { - var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-5-1"].Id); + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-5-1"].Id); var siblings = UserStartNodeEntitiesService .SiblingUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["1-5"].Key, + ItemsByName["1-5"].Key, 2, 2, BySortOrder, @@ -144,7 +144,7 @@ public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetGrandchild_Yi Assert.AreEqual(1, siblings.Length); Assert.Multiple(() => { - Assert.AreEqual(_mediaByName[$"1-5"].Key, siblings[0].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-5"].Key, siblings[0].Entity.Key); Assert.IsFalse(siblings[0].HasAccess); }); } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.childUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.childUserAccessEntities.cs index 4df5ecbab71c..c68ed770b30c 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.childUserAccessEntities.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.childUserAccessEntities.cs @@ -8,13 +8,13 @@ public partial class UserStartNodeEntitiesServiceMediaTests [Test] public async Task ChildUserAccessEntities_FirstAndLastChildOfRoot_YieldsBothInFirstPage_AsAllowed() { - var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-1"].Id, _mediaByName["1-10"].Id); + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-1"].Id, ItemsByName["1-10"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["1"].Key, + ItemsByName["1"].Key, 0, 3, BySortOrder, @@ -27,8 +27,8 @@ public async Task ChildUserAccessEntities_FirstAndLastChildOfRoot_YieldsBothInFi Assert.Multiple(() => { // first and last media items are the ones allowed - Assert.AreEqual(_mediaByName["1-1"].Key, children[0].Entity.Key); - Assert.AreEqual(_mediaByName["1-10"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["1-1"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["1-10"].Key, children[1].Entity.Key); // explicitly verify the entity sort order, both so we know sorting works, // and so we know it's actually the first and last item below "1" @@ -44,14 +44,14 @@ public async Task ChildUserAccessEntities_FirstAndLastChildOfRoot_YieldsBothInFi [Test] public async Task ChildUserAccessEntities_InAndOutOfScope_YieldsOnlyChildrenInScope() { - var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-5"].Id, _mediaByName["2-10"].Id); + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-5"].Id, ItemsByName["2-10"].Id); Assert.AreEqual(2, mediaStartNodePaths.Length); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["2"].Key, + ItemsByName["2"].Key, 0, 10, BySortOrder, @@ -63,7 +63,7 @@ public async Task ChildUserAccessEntities_InAndOutOfScope_YieldsOnlyChildrenInSc Assert.Multiple(() => { // only the "2-10" media item is returned, as "1-5" is out of scope - Assert.AreEqual(_mediaByName["2-10"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["2-10"].Key, children[0].Entity.Key); Assert.IsTrue(children[0].HasAccess); }); } @@ -71,14 +71,14 @@ public async Task ChildUserAccessEntities_InAndOutOfScope_YieldsOnlyChildrenInSc [Test] public async Task ChildUserAccessEntities_OutOfScope_YieldsNothing() { - var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-5"].Id, _mediaByName["2-10"].Id); + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-5"].Id, ItemsByName["2-10"].Id); Assert.AreEqual(2, mediaStartNodePaths.Length); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["3"].Key, + ItemsByName["3"].Key, 0, 10, BySortOrder, @@ -93,17 +93,17 @@ public async Task ChildUserAccessEntities_OutOfScope_YieldsNothing() public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginate() { var mediaStartNodePaths = await CreateUserAndGetStartNodePaths( - _mediaByName["1-1"].Id, - _mediaByName["1-3"].Id, - _mediaByName["1-5"].Id, - _mediaByName["1-7"].Id, - _mediaByName["1-9"].Id); + ItemsByName["1-1"].Id, + ItemsByName["1-3"].Id, + ItemsByName["1-5"].Id, + ItemsByName["1-7"].Id, + ItemsByName["1-9"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["1"].Key, + ItemsByName["1"].Key, 0, 2, BySortOrder, @@ -115,8 +115,8 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat Assert.AreEqual(2, children.Length); Assert.Multiple(() => { - Assert.AreEqual(_mediaByName["1-1"].Key, children[0].Entity.Key); - Assert.AreEqual(_mediaByName["1-3"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["1-1"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["1-3"].Key, children[1].Entity.Key); Assert.IsTrue(children[0].HasAccess); Assert.IsTrue(children[1].HasAccess); }); @@ -126,7 +126,7 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["1"].Key, + ItemsByName["1"].Key, 2, 2, BySortOrder, @@ -138,8 +138,8 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat Assert.AreEqual(2, children.Length); Assert.Multiple(() => { - Assert.AreEqual(_mediaByName["1-5"].Key, children[0].Entity.Key); - Assert.AreEqual(_mediaByName["1-7"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["1-5"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["1-7"].Key, children[1].Entity.Key); Assert.IsTrue(children[0].HasAccess); Assert.IsTrue(children[1].HasAccess); }); @@ -149,7 +149,7 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["1"].Key, + ItemsByName["1"].Key, 4, 2, BySortOrder, @@ -161,7 +161,7 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat Assert.AreEqual(1, children.Length); Assert.Multiple(() => { - Assert.AreEqual(_mediaByName["1-9"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["1-9"].Key, children[0].Entity.Key); Assert.IsTrue(children[0].HasAccess); }); } @@ -169,13 +169,13 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat [Test] public async Task ChildUserAccessEntities_ChildrenAsStartNode_YieldsAllGrandchildren_AsAllowed() { - var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3"].Id); + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["3-3"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["3-3"].Key, + ItemsByName["3-3"].Key, 0, 100, BySortOrder, @@ -190,7 +190,7 @@ public async Task ChildUserAccessEntities_ChildrenAsStartNode_YieldsAllGrandchil foreach (var childNumber in Enumerable.Range(1, 5)) { var child = children[childNumber - 1]; - Assert.AreEqual(_mediaByName[$"3-3-{childNumber}"].Key, child.Entity.Key); + Assert.AreEqual(ItemsByName[$"3-3-{childNumber}"].Key, child.Entity.Key); Assert.IsTrue(child.HasAccess); } }); @@ -199,13 +199,13 @@ public async Task ChildUserAccessEntities_ChildrenAsStartNode_YieldsAllGrandchil [Test] public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchildren_AsAllowed() { - var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3-3"].Id, _mediaByName["3-3-4"].Id); + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["3-3-3"].Id, ItemsByName["3-3-4"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["3-3"].Key, + ItemsByName["3-3"].Key, 0, 3, BySortOrder, @@ -217,8 +217,8 @@ public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandch Assert.Multiple(() => { // the two items are the children of "3-3" - that is, the actual start nodes - Assert.AreEqual(_mediaByName["3-3-3"].Key, children[0].Entity.Key); - Assert.AreEqual(_mediaByName["3-3-4"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["3-3-3"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["3-3-4"].Key, children[1].Entity.Key); // both are allowed (they are the actual start nodes) Assert.IsTrue(children[0].HasAccess); @@ -229,13 +229,13 @@ public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandch [Test] public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsChildren_AsNotAllowed() { - var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3-3"].Id, _mediaByName["3-5-3"].Id); + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["3-3-3"].Id, ItemsByName["3-5-3"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["3"].Key, + ItemsByName["3"].Key, 0, 3, BySortOrder, @@ -247,8 +247,8 @@ public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsChildre Assert.Multiple(() => { // the two items are the children of "3" - that is, the parents of the actual start nodes - Assert.AreEqual(_mediaByName["3-3"].Key, children[0].Entity.Key); - Assert.AreEqual(_mediaByName["3-5"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["3-3"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["3-5"].Key, children[1].Entity.Key); // both are disallowed - only the two children (the actual start nodes) are allowed Assert.IsFalse(children[0].HasAccess); @@ -264,14 +264,14 @@ public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOn // and the ancestor start node is ignored. this differs somewhat from the norm; we normally consider // permissions additive (which in this case would mean ignoring the descendant rather than the ancestor). - var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3"].Id, _mediaByName["3-3-1"].Id, _mediaByName["3-3-5"].Id); + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["3-3"].Id, ItemsByName["3-3-1"].Id, ItemsByName["3-3-5"].Id); Assert.AreEqual(2, mediaStartNodePaths.Length); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["3"].Key, + ItemsByName["3"].Key, 0, 10, BySortOrder, @@ -281,7 +281,7 @@ public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOn Assert.AreEqual(1, children.Length); Assert.Multiple(() => { - Assert.AreEqual(_mediaByName["3-3"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["3-3"].Key, children[0].Entity.Key); Assert.IsFalse(children[0].HasAccess); }); @@ -289,7 +289,7 @@ public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOn .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["3-3"].Key, + ItemsByName["3-3"].Key, 0, 10, BySortOrder, @@ -301,8 +301,8 @@ public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOn Assert.Multiple(() => { // the two items are the children of "3-3" - that is, the actual start nodes - Assert.AreEqual(_mediaByName["3-3-1"].Key, children[0].Entity.Key); - Assert.AreEqual(_mediaByName["3-3-5"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["3-3-1"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["3-3-5"].Key, children[1].Entity.Key); Assert.IsTrue(children[0].HasAccess); Assert.IsTrue(children[1].HasAccess); @@ -312,13 +312,13 @@ public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOn [Test] public async Task ChildUserAccessEntities_ReverseStartNodeOrder_DoesNotAffectResultOrder() { - var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3"].Id, _mediaByName["3-2"].Id, _mediaByName["3-1"].Id); + var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["3-3"].Id, ItemsByName["3-2"].Id, ItemsByName["3-1"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Media, mediaStartNodePaths, - _mediaByName["3"].Key, + ItemsByName["3"].Key, 0, 10, BySortOrder, @@ -328,9 +328,9 @@ public async Task ChildUserAccessEntities_ReverseStartNodeOrder_DoesNotAffectRes Assert.AreEqual(3, children.Length); Assert.Multiple(() => { - Assert.AreEqual(_mediaByName["3-1"].Key, children[0].Entity.Key); - Assert.AreEqual(_mediaByName["3-2"].Key, children[1].Entity.Key); - Assert.AreEqual(_mediaByName["3-3"].Key, children[2].Entity.Key); + Assert.AreEqual(ItemsByName["3-1"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["3-2"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["3-3"].Key, children[2].Entity.Key); }); } } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.cs index 241dd1b604ff..c34730344a6f 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceMediaTests.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; -using Umbraco.Cms.Api.Management.Services.Entities; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; @@ -8,46 +6,22 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; -using Umbraco.Cms.Tests.Common.Testing; -using Umbraco.Cms.Tests.Integration.Testing; namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; [TestFixture] -[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] -public partial class UserStartNodeEntitiesServiceMediaTests : UmbracoIntegrationTest +public partial class UserStartNodeEntitiesServiceMediaTests : UserStartNodeEntitiesServiceTestsBase { - private Dictionary _mediaByName = new (); - private IUserGroup _userGroup; - private IMediaService MediaService => GetRequiredService(); private IMediaTypeService MediaTypeService => GetRequiredService(); - private IUserGroupService UserGroupService => GetRequiredService(); - - private IUserService UserService => GetRequiredService(); - - private IEntityService EntityService => GetRequiredService(); + protected override UmbracoObjectTypes ObjectType => UmbracoObjectTypes.Media; - private IUserStartNodeEntitiesService UserStartNodeEntitiesService => GetRequiredService(); - - protected readonly Ordering BySortOrder = Ordering.By("sortOrder"); - - protected override void ConfigureTestServices(IServiceCollection services) - { - base.ConfigureTestServices(services); - services.AddTransient(); - } + protected override string SectionAlias => "media"; - [SetUp] - public async Task SetUpTestAsync() + protected override async Task CreateContentTypeAndHierarchy() { - if (_mediaByName.Any()) - { - return; - } - var mediaType = new MediaTypeBuilder() .WithAlias("theMediaType") .Build(); @@ -63,7 +37,7 @@ public async Task SetUpTestAsync() .WithName($"{rootNumber}") .Build(); MediaService.Save(root); - _mediaByName[root.Name!] = root; + ItemsByName[root.Name!] = (root.Id, root.Key); foreach (var childNumber in Enumerable.Range(1, 10)) { @@ -73,7 +47,7 @@ public async Task SetUpTestAsync() .Build(); child.SetParent(root); MediaService.Save(child); - _mediaByName[child.Name!] = child; + ItemsByName[child.Name!] = (child.Id, child.Key); foreach (var grandChildNumber in Enumerable.Range(1, 5)) { @@ -83,52 +57,24 @@ public async Task SetUpTestAsync() .Build(); grandchild.SetParent(child); MediaService.Save(grandchild); - _mediaByName[grandchild.Name!] = grandchild; + ItemsByName[grandchild.Name!] = (grandchild.Id, grandchild.Key); } } } - - _userGroup = new UserGroupBuilder() - .WithAlias("theGroup") - .WithAllowedSections(["media"]) - .Build(); - _userGroup.StartMediaId = null; - await UserGroupService.CreateAsync(_userGroup, Constants.Security.SuperUserKey); } - private async Task CreateUserAndGetStartNodePaths(params int[] startNodeIds) - { - var user = await CreateUser(startNodeIds); - - var mediaStartNodePaths = user.GetMediaStartNodePaths(EntityService, AppCaches.NoCache); - Assert.IsNotNull(mediaStartNodePaths); - - return mediaStartNodePaths; - } - - private async Task CreateUserAndGetStartNodeIds(params int[] startNodeIds) - { - var user = await CreateUser(startNodeIds); + protected override void ClearUserGroupStartNode(IUserGroup userGroup) + => userGroup.StartMediaId = null; - var mediaStartNodeIds = user.CalculateMediaStartNodeIds(EntityService, AppCaches.NoCache); - Assert.IsNotNull(mediaStartNodeIds); - - return mediaStartNodeIds; - } - - private async Task CreateUser(int[] startNodeIds) - { - var user = new UserBuilder() + protected override Core.Models.Membership.User BuildUserWithStartNodes(int[] startNodeIds) + => new UserBuilder() .WithName(Guid.NewGuid().ToString("N")) .WithStartMediaIds(startNodeIds) .Build(); - UserService.Save(user); - var attempt = await UserGroupService.AddUsersToUserGroupAsync( - new UsersToUserGroupManipulationModel(_userGroup.Key, [user.Key]), - Constants.Security.SuperUserKey); + protected override string[]? GetStartNodePaths(Core.Models.Membership.User user) + => user.GetMediaStartNodePaths(EntityService, AppCaches.NoCache); - Assert.IsTrue(attempt.Success); - return user; - } + protected override int[]? CalculateStartNodeIds(Core.Models.Membership.User user) + => user.CalculateMediaStartNodeIds(EntityService, AppCaches.NoCache); } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.ChildUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.ChildUserAccessEntities.cs index c64b61810fc0..c25efea18e81 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.ChildUserAccessEntities.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.ChildUserAccessEntities.cs @@ -8,13 +8,13 @@ public partial class UserStartNodeEntitiesServiceTests [Test] public async Task ChildUserAccessEntities_FirstAndLastChildOfRoot_YieldsBothInFirstPage_AsAllowed() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-1"].Id, _contentByName["1-10"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-1"].Id, ItemsByName["1-10"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["1"].Key, + ItemsByName["1"].Key, 0, 3, BySortOrder, @@ -27,8 +27,8 @@ public async Task ChildUserAccessEntities_FirstAndLastChildOfRoot_YieldsBothInFi Assert.Multiple(() => { // first and last content items are the ones allowed - Assert.AreEqual(_contentByName["1-1"].Key, children[0].Entity.Key); - Assert.AreEqual(_contentByName["1-10"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["1-1"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["1-10"].Key, children[1].Entity.Key); // explicitly verify the entity sort order, both so we know sorting works, // and so we know it's actually the first and last item below "1" @@ -44,14 +44,14 @@ public async Task ChildUserAccessEntities_FirstAndLastChildOfRoot_YieldsBothInFi [Test] public async Task ChildUserAccessEntities_InAndOutOfScope_YieldsOnlyChildrenInScope() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-5"].Id, _contentByName["2-10"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-5"].Id, ItemsByName["2-10"].Id); Assert.AreEqual(2, contentStartNodePaths.Length); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["2"].Key, + ItemsByName["2"].Key, 0, 10, BySortOrder, @@ -63,7 +63,7 @@ public async Task ChildUserAccessEntities_InAndOutOfScope_YieldsOnlyChildrenInSc Assert.Multiple(() => { // only the "2-10" content item is returned, as "1-5" is out of scope - Assert.AreEqual(_contentByName["2-10"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["2-10"].Key, children[0].Entity.Key); Assert.IsTrue(children[0].HasAccess); }); } @@ -71,14 +71,14 @@ public async Task ChildUserAccessEntities_InAndOutOfScope_YieldsOnlyChildrenInSc [Test] public async Task ChildUserAccessEntities_OutOfScope_YieldsNothing() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-5"].Id, _contentByName["2-10"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-5"].Id, ItemsByName["2-10"].Id); Assert.AreEqual(2, contentStartNodePaths.Length); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["3"].Key, + ItemsByName["3"].Key, 0, 10, BySortOrder, @@ -93,17 +93,17 @@ public async Task ChildUserAccessEntities_OutOfScope_YieldsNothing() public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginate() { var contentStartNodePaths = await CreateUserAndGetStartNodePaths( - _contentByName["1-1"].Id, - _contentByName["1-3"].Id, - _contentByName["1-5"].Id, - _contentByName["1-7"].Id, - _contentByName["1-9"].Id); + ItemsByName["1-1"].Id, + ItemsByName["1-3"].Id, + ItemsByName["1-5"].Id, + ItemsByName["1-7"].Id, + ItemsByName["1-9"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["1"].Key, + ItemsByName["1"].Key, 0, 2, BySortOrder, @@ -115,8 +115,8 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat Assert.AreEqual(2, children.Length); Assert.Multiple(() => { - Assert.AreEqual(_contentByName["1-1"].Key, children[0].Entity.Key); - Assert.AreEqual(_contentByName["1-3"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["1-1"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["1-3"].Key, children[1].Entity.Key); Assert.IsTrue(children[0].HasAccess); Assert.IsTrue(children[1].HasAccess); }); @@ -126,7 +126,7 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["1"].Key, + ItemsByName["1"].Key, 2, 2, BySortOrder, @@ -138,8 +138,8 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat Assert.AreEqual(2, children.Length); Assert.Multiple(() => { - Assert.AreEqual(_contentByName["1-5"].Key, children[0].Entity.Key); - Assert.AreEqual(_contentByName["1-7"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["1-5"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["1-7"].Key, children[1].Entity.Key); Assert.IsTrue(children[0].HasAccess); Assert.IsTrue(children[1].HasAccess); }); @@ -149,7 +149,7 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["1"].Key, + ItemsByName["1"].Key, 4, 2, BySortOrder, @@ -161,7 +161,7 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat Assert.AreEqual(1, children.Length); Assert.Multiple(() => { - Assert.AreEqual(_contentByName["1-9"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["1-9"].Key, children[0].Entity.Key); Assert.IsTrue(children[0].HasAccess); }); } @@ -169,13 +169,13 @@ public async Task ChildUserAccessEntities_SpanningMultipleResultPages_CanPaginat [Test] public async Task ChildUserAccessEntities_ChildrenAsStartNode_YieldsAllGrandchildren_AsAllowed() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["3-3"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["3-3"].Key, + ItemsByName["3-3"].Key, 0, 100, BySortOrder, @@ -190,7 +190,7 @@ public async Task ChildUserAccessEntities_ChildrenAsStartNode_YieldsAllGrandchil foreach (var childNumber in Enumerable.Range(1, 5)) { var child = children[childNumber - 1]; - Assert.AreEqual(_contentByName[$"3-3-{childNumber}"].Key, child.Entity.Key); + Assert.AreEqual(ItemsByName[$"3-3-{childNumber}"].Key, child.Entity.Key); Assert.IsTrue(child.HasAccess); } }); @@ -199,13 +199,13 @@ public async Task ChildUserAccessEntities_ChildrenAsStartNode_YieldsAllGrandchil [Test] public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchildren_AsAllowed() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3-3"].Id, _contentByName["3-3-4"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["3-3-3"].Id, ItemsByName["3-3-4"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["3-3"].Key, + ItemsByName["3-3"].Key, 0, 3, BySortOrder, @@ -217,8 +217,8 @@ public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandch Assert.Multiple(() => { // the two items are the children of "3-3" - that is, the actual start nodes - Assert.AreEqual(_contentByName["3-3-3"].Key, children[0].Entity.Key); - Assert.AreEqual(_contentByName["3-3-4"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["3-3-3"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["3-3-4"].Key, children[1].Entity.Key); // both are allowed (they are the actual start nodes) Assert.IsTrue(children[0].HasAccess); @@ -229,13 +229,13 @@ public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandch [Test] public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsChildren_AsNotAllowed() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3-3"].Id, _contentByName["3-5-3"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["3-3-3"].Id, ItemsByName["3-5-3"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["3"].Key, + ItemsByName["3"].Key, 0, 3, BySortOrder, @@ -247,8 +247,8 @@ public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsChildre Assert.Multiple(() => { // the two items are the children of "3" - that is, the parents of the actual start nodes - Assert.AreEqual(_contentByName["3-3"].Key, children[0].Entity.Key); - Assert.AreEqual(_contentByName["3-5"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["3-3"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["3-5"].Key, children[1].Entity.Key); // both are disallowed - only the two children (the actual start nodes) are allowed Assert.IsFalse(children[0].HasAccess); @@ -264,14 +264,14 @@ public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOn // and the ancestor start node is ignored. this differs somewhat from the norm; we normally consider // permissions additive (which in this case would mean ignoring the descendant rather than the ancestor). - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3"].Id, _contentByName["3-3-1"].Id, _contentByName["3-3-5"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["3-3"].Id, ItemsByName["3-3-1"].Id, ItemsByName["3-3-5"].Id); Assert.AreEqual(2, contentStartNodePaths.Length); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["3"].Key, + ItemsByName["3"].Key, 0, 10, BySortOrder, @@ -281,7 +281,7 @@ public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOn Assert.AreEqual(1, children.Length); Assert.Multiple(() => { - Assert.AreEqual(_contentByName["3-3"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["3-3"].Key, children[0].Entity.Key); Assert.IsFalse(children[0].HasAccess); }); @@ -289,7 +289,7 @@ public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOn .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["3-3"].Key, + ItemsByName["3-3"].Key, 0, 10, BySortOrder, @@ -301,8 +301,8 @@ public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOn Assert.Multiple(() => { // the two items are the children of "3-3" - that is, the actual start nodes - Assert.AreEqual(_contentByName["3-3-1"].Key, children[0].Entity.Key); - Assert.AreEqual(_contentByName["3-3-5"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["3-3-1"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["3-3-5"].Key, children[1].Entity.Key); Assert.IsTrue(children[0].HasAccess); Assert.IsTrue(children[1].HasAccess); @@ -312,13 +312,13 @@ public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOn [Test] public async Task ChildUserAccessEntities_ReverseStartNodeOrder_DoesNotAffectResultOrder() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3"].Id, _contentByName["3-2"].Id, _contentByName["3-1"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["3-3"].Id, ItemsByName["3-2"].Id, ItemsByName["3-1"].Id); var children = UserStartNodeEntitiesService .ChildUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["3"].Key, + ItemsByName["3"].Key, 0, 10, BySortOrder, @@ -328,9 +328,9 @@ public async Task ChildUserAccessEntities_ReverseStartNodeOrder_DoesNotAffectRes Assert.AreEqual(3, children.Length); Assert.Multiple(() => { - Assert.AreEqual(_contentByName["3-1"].Key, children[0].Entity.Key); - Assert.AreEqual(_contentByName["3-2"].Key, children[1].Entity.Key); - Assert.AreEqual(_contentByName["3-3"].Key, children[2].Entity.Key); + Assert.AreEqual(ItemsByName["3-1"].Key, children[0].Entity.Key); + Assert.AreEqual(ItemsByName["3-2"].Key, children[1].Entity.Key); + Assert.AreEqual(ItemsByName["3-3"].Key, children[2].Entity.Key); }); } } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.RootUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.RootUserAccessEntities.cs index c73ac2778b8c..b1a5b236c1ab 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.RootUserAccessEntities.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.RootUserAccessEntities.cs @@ -8,7 +8,7 @@ public partial class UserStartNodeEntitiesServiceTests [Test] public async Task RootUserAccessEntities_FirstAndLastRoot_YieldsBoth_AsAllowed() { - var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_contentByName["1"].Id, _contentByName["5"].Id); + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(ItemsByName["1"].Id, ItemsByName["5"].Id); var roots = UserStartNodeEntitiesService .RootUserAccessEntities( @@ -21,8 +21,8 @@ public async Task RootUserAccessEntities_FirstAndLastRoot_YieldsBoth_AsAllowed() Assert.Multiple(() => { // first and last content items are the ones allowed - Assert.AreEqual(_contentByName["1"].Key, roots[0].Entity.Key); - Assert.AreEqual(_contentByName["5"].Key, roots[1].Entity.Key); + Assert.AreEqual(ItemsByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(ItemsByName["5"].Key, roots[1].Entity.Key); // explicitly verify the entity sort order, both so we know sorting works, // and so we know it's actually the first and last item at root @@ -38,7 +38,7 @@ public async Task RootUserAccessEntities_FirstAndLastRoot_YieldsBoth_AsAllowed() [Test] public async Task RootUserAccessEntities_ChildrenAsStartNode_YieldsChildRoots_AsNotAllowed() { - var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_contentByName["1-3"].Id, _contentByName["3-3"].Id, _contentByName["5-3"].Id); + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(ItemsByName["1-3"].Id, ItemsByName["3-3"].Id, ItemsByName["5-3"].Id); var roots = UserStartNodeEntitiesService .RootUserAccessEntities( @@ -50,9 +50,9 @@ public async Task RootUserAccessEntities_ChildrenAsStartNode_YieldsChildRoots_As Assert.Multiple(() => { // the three start nodes are the children of the "1", "3" and "5" roots, respectively, so these are expected as roots - Assert.AreEqual(_contentByName["1"].Key, roots[0].Entity.Key); - Assert.AreEqual(_contentByName["3"].Key, roots[1].Entity.Key); - Assert.AreEqual(_contentByName["5"].Key, roots[2].Entity.Key); + Assert.AreEqual(ItemsByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(ItemsByName["3"].Key, roots[1].Entity.Key); + Assert.AreEqual(ItemsByName["5"].Key, roots[2].Entity.Key); // all are disallowed - only the children (the actual start nodes) are allowed Assert.IsTrue(roots.All(r => r.HasAccess is false)); @@ -62,7 +62,7 @@ public async Task RootUserAccessEntities_ChildrenAsStartNode_YieldsChildRoots_As [Test] public async Task RootUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchildRoots_AsNotAllowed() { - var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_contentByName["1-2-3"].Id, _contentByName["2-3-4"].Id, _contentByName["3-4-5"].Id); + var contentStartNodeIds = await CreateUserAndGetStartNodeIds(ItemsByName["1-2-3"].Id, ItemsByName["2-3-4"].Id, ItemsByName["3-4-5"].Id); var roots = UserStartNodeEntitiesService .RootUserAccessEntities( @@ -74,9 +74,9 @@ public async Task RootUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchi Assert.Multiple(() => { // the three start nodes are the grandchildren of the "1", "2" and "3" roots, respectively, so these are expected as roots - Assert.AreEqual(_contentByName["1"].Key, roots[0].Entity.Key); - Assert.AreEqual(_contentByName["2"].Key, roots[1].Entity.Key); - Assert.AreEqual(_contentByName["3"].Key, roots[2].Entity.Key); + Assert.AreEqual(ItemsByName["1"].Key, roots[0].Entity.Key); + Assert.AreEqual(ItemsByName["2"].Key, roots[1].Entity.Key); + Assert.AreEqual(ItemsByName["3"].Key, roots[2].Entity.Key); // all are disallowed - only the grandchildren (the actual start nodes) are allowed Assert.IsTrue(roots.All(r => r.HasAccess is false)); diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.SiblingUserAccessEntities.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.SiblingUserAccessEntities.cs index 9e19fa9800d8..164509167ed9 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.SiblingUserAccessEntities.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.SiblingUserAccessEntities.cs @@ -8,13 +8,13 @@ public partial class UserStartNodeEntitiesServiceTests [Test] public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParent_YieldsAll_AsAllowed() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1"].Id); var siblings = UserStartNodeEntitiesService .SiblingUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["1-5"].Key, + ItemsByName["1-5"].Key, 2, 2, BySortOrder, @@ -29,7 +29,7 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParent_YieldsAl { for (int i = 0; i < 4; i++) { - Assert.AreEqual(_contentByName[$"1-{i + 3}"].Key, siblings[i].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-{i + 3}"].Key, siblings[i].Entity.Key); Assert.IsTrue(siblings[i].HasAccess); } }); @@ -40,13 +40,13 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParentAndTarget { // See notes on ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOnlyGrandchild. - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1"].Id, _contentByName["1-5"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1"].Id, ItemsByName["1-5"].Id); var siblings = UserStartNodeEntitiesService .SiblingUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["1-5"].Key, + ItemsByName["1-5"].Key, 2, 2, BySortOrder, @@ -59,7 +59,7 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParentAndTarget Assert.AreEqual(1, siblings.Length); Assert.Multiple(() => { - Assert.AreEqual(_contentByName[$"1-5"].Key, siblings[0].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-5"].Key, siblings[0].Entity.Key); Assert.IsTrue(siblings[0].HasAccess); }); } @@ -67,13 +67,13 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParentAndTarget [Test] public async Task SiblingUserAccessEntities_WithStartNodeOfTarget_YieldsOnlyTarget_AsAllowed() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-5"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-5"].Id); var siblings = UserStartNodeEntitiesService .SiblingUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["1-5"].Key, + ItemsByName["1-5"].Key, 2, 2, BySortOrder, @@ -86,7 +86,7 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTarget_YieldsOnlyTarg Assert.AreEqual(1, siblings.Length); Assert.Multiple(() => { - Assert.AreEqual(_contentByName[$"1-5"].Key, siblings[0].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-5"].Key, siblings[0].Entity.Key); Assert.IsTrue(siblings[0].HasAccess); }); } @@ -94,13 +94,13 @@ public async Task SiblingUserAccessEntities_WithStartNodeOfTarget_YieldsOnlyTarg [Test] public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetAndSiblings_YieldsOnlyPermitted_AsAllowed() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-3"].Id, _contentByName["1-5"].Id, _contentByName["1-7"].Id, _contentByName["1-10"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-3"].Id, ItemsByName["1-5"].Id, ItemsByName["1-7"].Id, ItemsByName["1-10"].Id); var siblings = UserStartNodeEntitiesService .SiblingUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["1-5"].Key, + ItemsByName["1-5"].Key, 1, 1, BySortOrder, @@ -114,11 +114,11 @@ public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetAndSiblings_Y Assert.Multiple(() => { - Assert.AreEqual(_contentByName[$"1-3"].Key, siblings[0].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-3"].Key, siblings[0].Entity.Key); Assert.IsTrue(siblings[0].HasAccess); - Assert.AreEqual(_contentByName[$"1-5"].Key, siblings[1].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-5"].Key, siblings[1].Entity.Key); Assert.IsTrue(siblings[1].HasAccess); - Assert.AreEqual(_contentByName[$"1-7"].Key, siblings[2].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-7"].Key, siblings[2].Entity.Key); Assert.IsTrue(siblings[2].HasAccess); }); } @@ -126,13 +126,13 @@ public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetAndSiblings_Y [Test] public async Task SiblingUserAccessEntities_WithStartNodesOfTargetChild_YieldsTarget_AsNotAllowed() { - var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-5-1"].Id); + var contentStartNodePaths = await CreateUserAndGetStartNodePaths(ItemsByName["1-5-1"].Id); var siblings = UserStartNodeEntitiesService .SiblingUserAccessEntities( UmbracoObjectTypes.Document, contentStartNodePaths, - _contentByName["1-5"].Key, + ItemsByName["1-5"].Key, 2, 2, BySortOrder, @@ -145,7 +145,7 @@ public async Task SiblingUserAccessEntities_WithStartNodesOfTargetChild_YieldsTa Assert.AreEqual(1, siblings.Length); Assert.Multiple(() => { - Assert.AreEqual(_contentByName[$"1-5"].Key, siblings[0].Entity.Key); + Assert.AreEqual(ItemsByName[$"1-5"].Key, siblings[0].Entity.Key); Assert.IsFalse(siblings[0].HasAccess); }); } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.cs index e7ad544af6c7..7dd9c70c2e83 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTests.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; -using Umbraco.Cms.Api.Management.Services.Entities; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; @@ -8,46 +6,22 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; -using Umbraco.Cms.Tests.Common.Testing; -using Umbraco.Cms.Tests.Integration.Testing; namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; [TestFixture] -[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] -public partial class UserStartNodeEntitiesServiceTests : UmbracoIntegrationTest +public partial class UserStartNodeEntitiesServiceTests : UserStartNodeEntitiesServiceTestsBase { - private Dictionary _contentByName = new (); - private IUserGroup _userGroup; - private IContentService ContentService => GetRequiredService(); private IContentTypeService ContentTypeService => GetRequiredService(); - private IUserGroupService UserGroupService => GetRequiredService(); - - private IUserService UserService => GetRequiredService(); - - private IEntityService EntityService => GetRequiredService(); + protected override UmbracoObjectTypes ObjectType => UmbracoObjectTypes.Document; - private IUserStartNodeEntitiesService UserStartNodeEntitiesService => GetRequiredService(); - - protected static readonly Ordering BySortOrder = Ordering.By("sortOrder"); - - protected override void ConfigureTestServices(IServiceCollection services) - { - base.ConfigureTestServices(services); - services.AddTransient(); - } + protected override string SectionAlias => "content"; - [SetUp] - public async Task SetUpTestAsync() + protected override async Task CreateContentTypeAndHierarchy() { - if (_contentByName.Any()) - { - return; - } - var contentType = new ContentTypeBuilder() .WithAlias("theContentType") .Build(); @@ -63,7 +37,7 @@ public async Task SetUpTestAsync() .WithName($"{rootNumber}") .Build(); ContentService.Save(root); - _contentByName[root.Name!] = root; + ItemsByName[root.Name!] = (root.Id, root.Key); foreach (var childNumber in Enumerable.Range(1, 10)) { @@ -73,7 +47,7 @@ public async Task SetUpTestAsync() .WithName($"{rootNumber}-{childNumber}") .Build(); ContentService.Save(child); - _contentByName[child.Name!] = child; + ItemsByName[child.Name!] = (child.Id, child.Key); foreach (var grandChildNumber in Enumerable.Range(1, 5)) { @@ -83,52 +57,24 @@ public async Task SetUpTestAsync() .WithName($"{rootNumber}-{childNumber}-{grandChildNumber}") .Build(); ContentService.Save(grandchild); - _contentByName[grandchild.Name!] = grandchild; + ItemsByName[grandchild.Name!] = (grandchild.Id, grandchild.Key); } } } - - _userGroup = new UserGroupBuilder() - .WithAlias("theGroup") - .WithAllowedSections(["content"]) - .Build(); - _userGroup.StartContentId = null; - await UserGroupService.CreateAsync(_userGroup, Constants.Security.SuperUserKey); } - private async Task CreateUserAndGetStartNodePaths(params int[] startNodeIds) - { - var user = await CreateUser(startNodeIds); - - var contentStartNodePaths = user.GetContentStartNodePaths(EntityService, AppCaches.NoCache); - Assert.IsNotNull(contentStartNodePaths); - - return contentStartNodePaths; - } - - private async Task CreateUserAndGetStartNodeIds(params int[] startNodeIds) - { - var user = await CreateUser(startNodeIds); + protected override void ClearUserGroupStartNode(IUserGroup userGroup) + => userGroup.StartContentId = null; - var contentStartNodeIds = user.CalculateContentStartNodeIds(EntityService, AppCaches.NoCache); - Assert.IsNotNull(contentStartNodeIds); - - return contentStartNodeIds; - } - - private async Task CreateUser(int[] startNodeIds) - { - var user = new UserBuilder() + protected override Cms.Core.Models.Membership.User BuildUserWithStartNodes(int[] startNodeIds) + => new UserBuilder() .WithName(Guid.NewGuid().ToString("N")) .WithStartContentIds(startNodeIds) .Build(); - UserService.Save(user); - var attempt = await UserGroupService.AddUsersToUserGroupAsync( - new UsersToUserGroupManipulationModel(_userGroup.Key, [user.Key]), - Constants.Security.SuperUserKey); + protected override string[]? GetStartNodePaths(Cms.Core.Models.Membership.User user) + => user.GetContentStartNodePaths(EntityService, AppCaches.NoCache); - Assert.IsTrue(attempt.Success); - return user; - } + protected override int[]? CalculateStartNodeIds(Cms.Core.Models.Membership.User user) + => user.CalculateContentStartNodeIds(EntityService, AppCaches.NoCache); } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTestsBase.cs new file mode 100644 index 000000000000..5f6422b79a04 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/UserStartNodeEntitiesServiceTestsBase.cs @@ -0,0 +1,148 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Core; +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.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services; + +/// +/// Abstract base class for UserStartNodeEntitiesService tests. +/// Provides common setup, services, and helper methods for testing start node access +/// across different content types (Document, Media, Element). +/// +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] +public abstract class UserStartNodeEntitiesServiceTestsBase : UmbracoIntegrationTest +{ + /// + /// All items by name, storing just the Id and Key needed for tests. + /// + protected Dictionary ItemsByName { get; } = new(); + + protected IUserGroup UserGroup { get; set; } = null!; + + protected IUserGroupService UserGroupService => GetRequiredService(); + + protected IUserService UserService => GetRequiredService(); + + protected IEntityService EntityService => GetRequiredService(); + + protected IUserStartNodeEntitiesService UserStartNodeEntitiesService => GetRequiredService(); + + protected static readonly Ordering BySortOrder = Ordering.By("sortOrder"); + + /// + /// Gets the UmbracoObjectType for the content type being tested. + /// + protected abstract UmbracoObjectTypes ObjectType { get; } + + /// + /// Gets the section alias for the user group (e.g., "content", "media", "element"). + /// + protected abstract string SectionAlias { get; } + + protected override void ConfigureTestServices(IServiceCollection services) + { + base.ConfigureTestServices(services); + services.AddTransient(); + } + + [SetUp] + public async Task SetUpTestAsync() + { + if (ItemsByName.Any()) + { + return; + } + + await CreateContentTypeAndHierarchy(); + await CreateUserGroup(); + } + + /// + /// Creates the content type and a hierarchy of items: + /// 5 roots, each with 10 children, each with 5 grandchildren (300 total items). + /// Items are named by their position: "1", "1-1", "1-1-1", etc. + /// + protected abstract Task CreateContentTypeAndHierarchy(); + + /// + /// Creates the user group for testing. + /// + protected virtual async Task CreateUserGroup() + { + UserGroup = new UserGroupBuilder() + .WithAlias(Guid.NewGuid().ToString("N")) + .WithAllowedSections([SectionAlias]) + .Build(); + + ClearUserGroupStartNode(UserGroup); + await UserGroupService.CreateAsync(UserGroup, Constants.Security.SuperUserKey); + } + + /// + /// Clears the start node for the user group (type-specific implementation). + /// + protected abstract void ClearUserGroupStartNode(IUserGroup userGroup); + + /// + /// Creates a user with the specified start node IDs and returns the calculated start node paths. + /// + protected async Task CreateUserAndGetStartNodePaths(params int[] startNodeIds) + { + var user = await CreateUser(startNodeIds); + var paths = GetStartNodePaths(user); + Assert.IsNotNull(paths); + return paths; + } + + /// + /// Creates a user with the specified start node IDs and returns the calculated start node IDs. + /// + protected async Task CreateUserAndGetStartNodeIds(params int[] startNodeIds) + { + var user = await CreateUser(startNodeIds); + var ids = CalculateStartNodeIds(user); + Assert.IsNotNull(ids); + return ids; + } + + /// + /// Creates a user with the specified start node IDs. + /// + protected async Task CreateUser(int[] startNodeIds) + { + var user = BuildUserWithStartNodes(startNodeIds); + UserService.Save(user); + + var attempt = await UserGroupService.AddUsersToUserGroupAsync( + new UsersToUserGroupManipulationModel(UserGroup.Key, [user.Key]), + Constants.Security.SuperUserKey); + + Assert.IsTrue(attempt.Success); + return user; + } + + /// + /// Builds a user with the specified start node IDs (type-specific implementation). + /// + protected abstract Cms.Core.Models.Membership.User BuildUserWithStartNodes(int[] startNodeIds); + + /// + /// Gets the start node paths for the user (type-specific implementation). + /// + protected abstract string[]? GetStartNodePaths(Cms.Core.Models.Membership.User user); + + /// + /// Calculates the start node IDs for the user (type-specific implementation). + /// + protected abstract int[]? CalculateStartNodeIds(Cms.Core.Models.Membership.User user); +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index c8ef82746ba5..1d1b788b8fe4 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -361,5 +361,14 @@ ElementEditingServiceTests.cs + + UserStartNodeEntitiesServiceElementTests.cs + + + UserStartNodeEntitiesServiceElementTests.cs + + + UserStartNodeEntitiesServiceElementTests.cs + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs index 400ec8e6cb30..aab6817f2345 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Editors/UserEditorAuthorizationHelperTests.cs @@ -118,7 +118,7 @@ public bool Can_only_add_user_groups_you_are_part_of_yourself_unless_you_are_adm { var currentUser = Mock.Of(user => user.Groups == new[] { - new ReadOnlyUserGroup(1, Guid.NewGuid(), "CurrentUser", null, "icon-user", null, null, groupAlias, new int[0], new string[0], new HashSet(), new HashSet(), true), + new ReadOnlyUserGroup(1, Guid.NewGuid(), "CurrentUser", null, "icon-user", null, 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