Fix pagination for users restricted by start nodes (#18907)
* Fix pagination for users restricted by start nodes * Default implementation to avoid breakage * Review comments * Fix failing test * Add media start node tests
This commit is contained in:
@@ -42,11 +42,21 @@ public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControl
|
||||
|
||||
protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip, int take, out long totalItems)
|
||||
{
|
||||
IEntitySlim[] children = base.GetPagedChildEntities(parentKey, skip, take, out totalItems);
|
||||
return UserHasRootAccess() || IgnoreUserStartNodes()
|
||||
? children
|
||||
// Keeping the correct totalItems amount from GetPagedChildEntities
|
||||
: CalculateAccessMap(() => _userStartNodeEntitiesService.ChildUserAccessEntities(children, UserStartNodePaths), out _);
|
||||
if (UserHasRootAccess() || IgnoreUserStartNodes())
|
||||
{
|
||||
return base.GetPagedChildEntities(parentKey, skip, take, out totalItems);
|
||||
}
|
||||
|
||||
IEnumerable<UserAccessEntity> userAccessEntities = _userStartNodeEntitiesService.ChildUserAccessEntities(
|
||||
ItemObjectType,
|
||||
UserStartNodePaths,
|
||||
parentKey,
|
||||
skip,
|
||||
take,
|
||||
ItemOrdering,
|
||||
out totalItems);
|
||||
|
||||
return CalculateAccessMap(() => userAccessEntities, out _);
|
||||
}
|
||||
|
||||
protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
|
||||
|
||||
@@ -16,9 +16,41 @@ public interface IUserStartNodeEntitiesService
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the applicable child entities for a given object type for users without root access.
|
||||
/// </summary>
|
||||
/// <param name="umbracoObjectType">The object type.</param>
|
||||
/// <param name="userStartNodePaths">The calculated start node paths for the user.</param>
|
||||
/// <param name="parentKey">The key of the parent.</param>
|
||||
/// <param name="skip">The number of applicable children to skip.</param>
|
||||
/// <param name="take">The number of applicable children to take.</param>
|
||||
/// <param name="ordering">The ordering to apply when fetching and paginating the children.</param>
|
||||
/// <param name="totalItems">The total number of applicable children available.</param>
|
||||
/// <returns>A list of child entities applicable for the user.</returns>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
IEnumerable<UserAccessEntity> ChildUserAccessEntities(
|
||||
UmbracoObjectTypes umbracoObjectType,
|
||||
string[] userStartNodePaths,
|
||||
Guid parentKey,
|
||||
int skip,
|
||||
int take,
|
||||
Ordering ordering,
|
||||
out long totalItems)
|
||||
{
|
||||
totalItems = 0;
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the applicable child entities from a list of candidate child entities for users without root access.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
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.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Services.Entities;
|
||||
@@ -10,8 +14,24 @@ namespace Umbraco.Cms.Api.Management.Services.Entities;
|
||||
public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
|
||||
{
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly ICoreScopeProvider _scopeProvider;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
|
||||
public UserStartNodeEntitiesService(IEntityService entityService) => _entityService = entityService;
|
||||
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V17.")]
|
||||
public UserStartNodeEntitiesService(IEntityService entityService)
|
||||
: this(
|
||||
entityService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ICoreScopeProvider>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IIdKeyMap>())
|
||||
{
|
||||
}
|
||||
|
||||
public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProvider scopeProvider, IIdKeyMap idKeyMap)
|
||||
{
|
||||
_entityService = entityService;
|
||||
_scopeProvider = scopeProvider;
|
||||
_idKeyMap = idKeyMap;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds)
|
||||
@@ -43,6 +63,54 @@ public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(UmbracoObjectTypes umbracoObjectType, string[] userStartNodePaths, Guid parentKey, int skip, int take, Ordering ordering, out long totalItems)
|
||||
{
|
||||
Attempt<int> parentIdAttempt = _idKeyMap.GetIdForKey(parentKey, umbracoObjectType);
|
||||
if (parentIdAttempt.Success is false)
|
||||
{
|
||||
totalItems = 0;
|
||||
return [];
|
||||
}
|
||||
|
||||
var parentId = parentIdAttempt.Result;
|
||||
IEntitySlim? parent = _entityService.Get(parentId);
|
||||
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);
|
||||
}
|
||||
|
||||
// if one or more of the user start nodes are descendants of the requested parent, find the "next child IDs" in those user start node paths
|
||||
// - e.g. given the user start node path "-1,2,3,4,5", if the requested parent ID is 3, the "next child ID" is 4.
|
||||
var userStartNodePathIds = userStartNodePaths.Select(path => path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray();
|
||||
var allowedChildIds = userStartNodePathIds
|
||||
.Where(ids => ids.Contains(parentId))
|
||||
// given the previous checks, the parent ID can never be the last in the user start node path, so this is safe
|
||||
.Select(ids => ids[ids.IndexOf(parentId) + 1])
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
totalItems = allowedChildIds.Length;
|
||||
if (allowedChildIds.Length == 0)
|
||||
{
|
||||
// the requested parent is outside the scope of any user start nodes
|
||||
return [];
|
||||
}
|
||||
|
||||
// even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children
|
||||
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedChildIds.Contains(x.Id));
|
||||
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, query, ordering).ToArray();
|
||||
return ChildUserAccessEntities(children, userStartNodePaths);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> candidateChildren, string[] userStartNodePaths)
|
||||
// child entities for users without root access should include:
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services;
|
||||
|
||||
public partial class UserStartNodeEntitiesServiceMediaTests
|
||||
{
|
||||
[Test]
|
||||
public async Task RootUserAccessEntities_FirstAndLastRoot_YieldsBoth_AsAllowed()
|
||||
{
|
||||
var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_mediaByName["1"].Id, _mediaByName["5"].Id);
|
||||
|
||||
var roots = UserStartNodeEntitiesService
|
||||
.RootUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
contentStartNodeIds)
|
||||
.ToArray();
|
||||
|
||||
// expected total is 2, because only two items at root ("1" amd "10") are allowed
|
||||
Assert.AreEqual(2, roots.Length);
|
||||
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);
|
||||
|
||||
// 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
|
||||
Assert.AreEqual(0, roots[0].Entity.SortOrder);
|
||||
Assert.AreEqual(4, roots[1].Entity.SortOrder);
|
||||
|
||||
// both are allowed (they are the actual start nodes)
|
||||
Assert.IsTrue(roots[0].HasAccess);
|
||||
Assert.IsTrue(roots[1].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[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 roots = UserStartNodeEntitiesService
|
||||
.RootUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
contentStartNodeIds)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(3, roots.Length);
|
||||
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);
|
||||
|
||||
// all are disallowed - only the children (the actual start nodes) are allowed
|
||||
Assert.IsTrue(roots.All(r => r.HasAccess is false));
|
||||
});
|
||||
}
|
||||
|
||||
[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 roots = UserStartNodeEntitiesService
|
||||
.RootUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
contentStartNodeIds)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(3, roots.Length);
|
||||
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);
|
||||
|
||||
// all are disallowed - only the grandchildren (the actual start nodes) are allowed
|
||||
Assert.IsTrue(roots.All(r => r.HasAccess is false));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services;
|
||||
|
||||
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 children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["1"].Key,
|
||||
0,
|
||||
3,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
// expected total is 2, because only two items under "1" 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 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);
|
||||
|
||||
// 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"
|
||||
Assert.AreEqual(0, children[0].Entity.SortOrder);
|
||||
Assert.AreEqual(9, children[1].Entity.SortOrder);
|
||||
|
||||
// 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()
|
||||
{
|
||||
var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-5"].Id, _mediaByName["2-10"].Id);
|
||||
Assert.AreEqual(2, mediaStartNodePaths.Length);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["2"].Key,
|
||||
0,
|
||||
10,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(1, totalItems);
|
||||
Assert.AreEqual(1, children.Length);
|
||||
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.IsTrue(children[0].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_OutOfScope_YieldsNothing()
|
||||
{
|
||||
var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-5"].Id, _mediaByName["2-10"].Id);
|
||||
Assert.AreEqual(2, mediaStartNodePaths.Length);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["3"].Key,
|
||||
0,
|
||||
10,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(0, totalItems);
|
||||
Assert.AreEqual(0, children.Length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
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);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["1"].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(_mediaByName["1-1"].Key, children[0].Entity.Key);
|
||||
Assert.AreEqual(_mediaByName["1-3"].Key, children[1].Entity.Key);
|
||||
Assert.IsTrue(children[0].HasAccess);
|
||||
Assert.IsTrue(children[1].HasAccess);
|
||||
});
|
||||
|
||||
// next result page
|
||||
children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["1"].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(_mediaByName["1-5"].Key, children[0].Entity.Key);
|
||||
Assert.AreEqual(_mediaByName["1-7"].Key, children[1].Entity.Key);
|
||||
Assert.IsTrue(children[0].HasAccess);
|
||||
Assert.IsTrue(children[1].HasAccess);
|
||||
});
|
||||
|
||||
// next result page
|
||||
children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["1"].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(_mediaByName["1-9"].Key, children[0].Entity.Key);
|
||||
Assert.IsTrue(children[0].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_ChildrenAsStartNode_YieldsAllGrandchildren_AsAllowed()
|
||||
{
|
||||
var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3"].Id);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["3-3"].Key,
|
||||
0,
|
||||
100,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(5, totalItems);
|
||||
Assert.AreEqual(5, children.Length);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// all children of "3-3" should be allowed because "3-3" is a start node
|
||||
foreach (var childNumber in Enumerable.Range(1, 5))
|
||||
{
|
||||
var child = children[childNumber - 1];
|
||||
Assert.AreEqual(_mediaByName[$"3-3-{childNumber}"].Key, child.Entity.Key);
|
||||
Assert.IsTrue(child.HasAccess);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchildren_AsAllowed()
|
||||
{
|
||||
var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3-3"].Id, _mediaByName["3-3-4"].Id);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["3-3"].Key,
|
||||
0,
|
||||
3,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(2, totalItems);
|
||||
Assert.AreEqual(2, children.Length);
|
||||
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);
|
||||
|
||||
// both are allowed (they are the actual start nodes)
|
||||
Assert.IsTrue(children[0].HasAccess);
|
||||
Assert.IsTrue(children[1].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsChildren_AsNotAllowed()
|
||||
{
|
||||
var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3-3"].Id, _mediaByName["3-5-3"].Id);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["3"].Key,
|
||||
0,
|
||||
3,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(2, totalItems);
|
||||
Assert.AreEqual(2, children.Length);
|
||||
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);
|
||||
|
||||
// both are disallowed - only the two children (the actual start nodes) are allowed
|
||||
Assert.IsFalse(children[0].HasAccess);
|
||||
Assert.IsFalse(children[1].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOnlyGrandchild()
|
||||
{
|
||||
// NOTE: this test proves that start node paths are *not* additive when structural inheritance is in play.
|
||||
// if one has a start node that is a descendant to another start node, the descendant start node "wins"
|
||||
// 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);
|
||||
Assert.AreEqual(2, mediaStartNodePaths.Length);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["3"].Key,
|
||||
0,
|
||||
10,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
Assert.AreEqual(1, totalItems);
|
||||
Assert.AreEqual(1, children.Length);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(_mediaByName["3-3"].Key, children[0].Entity.Key);
|
||||
Assert.IsFalse(children[0].HasAccess);
|
||||
});
|
||||
|
||||
children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["3-3"].Key,
|
||||
0,
|
||||
10,
|
||||
BySortOrder,
|
||||
out totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(2, totalItems);
|
||||
Assert.AreEqual(2, children.Length);
|
||||
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.IsTrue(children[0].HasAccess);
|
||||
Assert.IsTrue(children[1].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_ReverseStartNodeOrder_DoesNotAffectResultOrder()
|
||||
{
|
||||
var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["3-3"].Id, _mediaByName["3-2"].Id, _mediaByName["3-1"].Id);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Media,
|
||||
mediaStartNodePaths,
|
||||
_mediaByName["3"].Key,
|
||||
0,
|
||||
10,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
Assert.AreEqual(3, totalItems);
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
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.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;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
|
||||
public partial class UserStartNodeEntitiesServiceMediaTests : UmbracoIntegrationTest
|
||||
{
|
||||
private Dictionary<string, IMedia> _mediaByName = new ();
|
||||
private IUserGroup _userGroup;
|
||||
|
||||
private IMediaService MediaService => GetRequiredService<IMediaService>();
|
||||
|
||||
private IMediaTypeService MediaTypeService => GetRequiredService<IMediaTypeService>();
|
||||
|
||||
private IUserGroupService UserGroupService => GetRequiredService<IUserGroupService>();
|
||||
|
||||
private IUserService UserService => GetRequiredService<IUserService>();
|
||||
|
||||
private IEntityService EntityService => GetRequiredService<IEntityService>();
|
||||
|
||||
private IUserStartNodeEntitiesService UserStartNodeEntitiesService => GetRequiredService<IUserStartNodeEntitiesService>();
|
||||
|
||||
protected readonly Ordering BySortOrder = Ordering.By("sortOrder");
|
||||
|
||||
protected override void ConfigureTestServices(IServiceCollection services)
|
||||
{
|
||||
base.ConfigureTestServices(services);
|
||||
services.AddTransient<IUserStartNodeEntitiesService, UserStartNodeEntitiesService>();
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public async Task SetUpTestAsync()
|
||||
{
|
||||
if (_mediaByName.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mediaType = new MediaTypeBuilder()
|
||||
.WithAlias("theMediaType")
|
||||
.Build();
|
||||
mediaType.AllowedAsRoot = true;
|
||||
await MediaTypeService.CreateAsync(mediaType, Constants.Security.SuperUserKey);
|
||||
mediaType.AllowedContentTypes = [new() { Alias = mediaType.Alias, Key = mediaType.Key }];
|
||||
await MediaTypeService.UpdateAsync(mediaType, Constants.Security.SuperUserKey);
|
||||
|
||||
foreach (var rootNumber in Enumerable.Range(1, 5))
|
||||
{
|
||||
var root = new MediaBuilder()
|
||||
.WithMediaType(mediaType)
|
||||
.WithName($"{rootNumber}")
|
||||
.Build();
|
||||
MediaService.Save(root);
|
||||
_mediaByName[root.Name!] = root;
|
||||
|
||||
foreach (var childNumber in Enumerable.Range(1, 10))
|
||||
{
|
||||
var child = new MediaBuilder()
|
||||
.WithMediaType(mediaType)
|
||||
.WithName($"{rootNumber}-{childNumber}")
|
||||
.Build();
|
||||
child.SetParent(root);
|
||||
MediaService.Save(child);
|
||||
_mediaByName[child.Name!] = child;
|
||||
|
||||
foreach (var grandChildNumber in Enumerable.Range(1, 5))
|
||||
{
|
||||
var grandchild = new MediaBuilder()
|
||||
.WithMediaType(mediaType)
|
||||
.WithName($"{rootNumber}-{childNumber}-{grandChildNumber}")
|
||||
.Build();
|
||||
grandchild.SetParent(child);
|
||||
MediaService.Save(grandchild);
|
||||
_mediaByName[grandchild.Name!] = grandchild;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_userGroup = new UserGroupBuilder()
|
||||
.WithAlias("theGroup")
|
||||
.WithAllowedSections(["media"])
|
||||
.Build();
|
||||
_userGroup.StartMediaId = null;
|
||||
await UserGroupService.CreateAsync(_userGroup, Constants.Security.SuperUserKey);
|
||||
}
|
||||
|
||||
private async Task<string[]> CreateUserAndGetStartNodePaths(params int[] startNodeIds)
|
||||
{
|
||||
var user = await CreateUser(startNodeIds);
|
||||
|
||||
var mediaStartNodePaths = user.GetMediaStartNodePaths(EntityService, AppCaches.NoCache);
|
||||
Assert.IsNotNull(mediaStartNodePaths);
|
||||
|
||||
return mediaStartNodePaths;
|
||||
}
|
||||
|
||||
private async Task<int[]> CreateUserAndGetStartNodeIds(params int[] startNodeIds)
|
||||
{
|
||||
var user = await CreateUser(startNodeIds);
|
||||
|
||||
var mediaStartNodeIds = user.CalculateMediaStartNodeIds(EntityService, AppCaches.NoCache);
|
||||
Assert.IsNotNull(mediaStartNodeIds);
|
||||
|
||||
return mediaStartNodeIds;
|
||||
}
|
||||
|
||||
private async Task<User> CreateUser(int[] startNodeIds)
|
||||
{
|
||||
var user = 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);
|
||||
|
||||
Assert.IsTrue(attempt.Success);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services;
|
||||
|
||||
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 children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["1"].Key,
|
||||
0,
|
||||
3,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
// expected total is 2, because only two items under "1" 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 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);
|
||||
|
||||
// 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"
|
||||
Assert.AreEqual(0, children[0].Entity.SortOrder);
|
||||
Assert.AreEqual(9, children[1].Entity.SortOrder);
|
||||
|
||||
// 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()
|
||||
{
|
||||
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-5"].Id, _contentByName["2-10"].Id);
|
||||
Assert.AreEqual(2, contentStartNodePaths.Length);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["2"].Key,
|
||||
0,
|
||||
10,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(1, totalItems);
|
||||
Assert.AreEqual(1, children.Length);
|
||||
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.IsTrue(children[0].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_OutOfScope_YieldsNothing()
|
||||
{
|
||||
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-5"].Id, _contentByName["2-10"].Id);
|
||||
Assert.AreEqual(2, contentStartNodePaths.Length);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["3"].Key,
|
||||
0,
|
||||
10,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(0, totalItems);
|
||||
Assert.AreEqual(0, children.Length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
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);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["1"].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(_contentByName["1-1"].Key, children[0].Entity.Key);
|
||||
Assert.AreEqual(_contentByName["1-3"].Key, children[1].Entity.Key);
|
||||
Assert.IsTrue(children[0].HasAccess);
|
||||
Assert.IsTrue(children[1].HasAccess);
|
||||
});
|
||||
|
||||
// next result page
|
||||
children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["1"].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(_contentByName["1-5"].Key, children[0].Entity.Key);
|
||||
Assert.AreEqual(_contentByName["1-7"].Key, children[1].Entity.Key);
|
||||
Assert.IsTrue(children[0].HasAccess);
|
||||
Assert.IsTrue(children[1].HasAccess);
|
||||
});
|
||||
|
||||
// next result page
|
||||
children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["1"].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(_contentByName["1-9"].Key, children[0].Entity.Key);
|
||||
Assert.IsTrue(children[0].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_ChildrenAsStartNode_YieldsAllGrandchildren_AsAllowed()
|
||||
{
|
||||
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3"].Id);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["3-3"].Key,
|
||||
0,
|
||||
100,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(5, totalItems);
|
||||
Assert.AreEqual(5, children.Length);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// all children of "3-3" should be allowed because "3-3" is a start node
|
||||
foreach (var childNumber in Enumerable.Range(1, 5))
|
||||
{
|
||||
var child = children[childNumber - 1];
|
||||
Assert.AreEqual(_contentByName[$"3-3-{childNumber}"].Key, child.Entity.Key);
|
||||
Assert.IsTrue(child.HasAccess);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsGrandchildren_AsAllowed()
|
||||
{
|
||||
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3-3"].Id, _contentByName["3-3-4"].Id);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["3-3"].Key,
|
||||
0,
|
||||
3,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(2, totalItems);
|
||||
Assert.AreEqual(2, children.Length);
|
||||
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);
|
||||
|
||||
// both are allowed (they are the actual start nodes)
|
||||
Assert.IsTrue(children[0].HasAccess);
|
||||
Assert.IsTrue(children[1].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_GrandchildrenAsStartNode_YieldsChildren_AsNotAllowed()
|
||||
{
|
||||
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3-3"].Id, _contentByName["3-5-3"].Id);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["3"].Key,
|
||||
0,
|
||||
3,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(2, totalItems);
|
||||
Assert.AreEqual(2, children.Length);
|
||||
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);
|
||||
|
||||
// both are disallowed - only the two children (the actual start nodes) are allowed
|
||||
Assert.IsFalse(children[0].HasAccess);
|
||||
Assert.IsFalse(children[1].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOnlyGrandchild()
|
||||
{
|
||||
// NOTE: this test proves that start node paths are *not* additive when structural inheritance is in play.
|
||||
// if one has a start node that is a descendant to another start node, the descendant start node "wins"
|
||||
// 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);
|
||||
Assert.AreEqual(2, contentStartNodePaths.Length);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["3"].Key,
|
||||
0,
|
||||
10,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
Assert.AreEqual(1, totalItems);
|
||||
Assert.AreEqual(1, children.Length);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(_contentByName["3-3"].Key, children[0].Entity.Key);
|
||||
Assert.IsFalse(children[0].HasAccess);
|
||||
});
|
||||
|
||||
children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["3-3"].Key,
|
||||
0,
|
||||
10,
|
||||
BySortOrder,
|
||||
out totalItems)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(2, totalItems);
|
||||
Assert.AreEqual(2, children.Length);
|
||||
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.IsTrue(children[0].HasAccess);
|
||||
Assert.IsTrue(children[1].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChildUserAccessEntities_ReverseStartNodeOrder_DoesNotAffectResultOrder()
|
||||
{
|
||||
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["3-3"].Id, _contentByName["3-2"].Id, _contentByName["3-1"].Id);
|
||||
|
||||
var children = UserStartNodeEntitiesService
|
||||
.ChildUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodePaths,
|
||||
_contentByName["3"].Key,
|
||||
0,
|
||||
10,
|
||||
BySortOrder,
|
||||
out var totalItems)
|
||||
.ToArray();
|
||||
Assert.AreEqual(3, totalItems);
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services;
|
||||
|
||||
public partial class UserStartNodeEntitiesServiceTests
|
||||
{
|
||||
[Test]
|
||||
public async Task RootUserAccessEntities_FirstAndLastRoot_YieldsBoth_AsAllowed()
|
||||
{
|
||||
var contentStartNodeIds = await CreateUserAndGetStartNodeIds(_contentByName["1"].Id, _contentByName["5"].Id);
|
||||
|
||||
var roots = UserStartNodeEntitiesService
|
||||
.RootUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodeIds)
|
||||
.ToArray();
|
||||
|
||||
// expected total is 2, because only two items at root ("1" amd "10") are allowed
|
||||
Assert.AreEqual(2, roots.Length);
|
||||
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);
|
||||
|
||||
// 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
|
||||
Assert.AreEqual(0, roots[0].Entity.SortOrder);
|
||||
Assert.AreEqual(4, roots[1].Entity.SortOrder);
|
||||
|
||||
// both are allowed (they are the actual start nodes)
|
||||
Assert.IsTrue(roots[0].HasAccess);
|
||||
Assert.IsTrue(roots[1].HasAccess);
|
||||
});
|
||||
}
|
||||
|
||||
[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 roots = UserStartNodeEntitiesService
|
||||
.RootUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodeIds)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(3, roots.Length);
|
||||
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);
|
||||
|
||||
// all are disallowed - only the children (the actual start nodes) are allowed
|
||||
Assert.IsTrue(roots.All(r => r.HasAccess is false));
|
||||
});
|
||||
}
|
||||
|
||||
[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 roots = UserStartNodeEntitiesService
|
||||
.RootUserAccessEntities(
|
||||
UmbracoObjectTypes.Document,
|
||||
contentStartNodeIds)
|
||||
.ToArray();
|
||||
|
||||
Assert.AreEqual(3, roots.Length);
|
||||
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);
|
||||
|
||||
// all are disallowed - only the grandchildren (the actual start nodes) are allowed
|
||||
Assert.IsTrue(roots.All(r => r.HasAccess is false));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
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.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;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
|
||||
public partial class UserStartNodeEntitiesServiceTests : UmbracoIntegrationTest
|
||||
{
|
||||
private Dictionary<string, IContent> _contentByName = new ();
|
||||
private IUserGroup _userGroup;
|
||||
|
||||
private IContentService ContentService => GetRequiredService<IContentService>();
|
||||
|
||||
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
|
||||
|
||||
private IUserGroupService UserGroupService => GetRequiredService<IUserGroupService>();
|
||||
|
||||
private IUserService UserService => GetRequiredService<IUserService>();
|
||||
|
||||
private IEntityService EntityService => GetRequiredService<IEntityService>();
|
||||
|
||||
private IUserStartNodeEntitiesService UserStartNodeEntitiesService => GetRequiredService<IUserStartNodeEntitiesService>();
|
||||
|
||||
protected readonly Ordering BySortOrder = Ordering.By("sortOrder");
|
||||
|
||||
protected override void ConfigureTestServices(IServiceCollection services)
|
||||
{
|
||||
base.ConfigureTestServices(services);
|
||||
services.AddTransient<IUserStartNodeEntitiesService, UserStartNodeEntitiesService>();
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public async Task SetUpTestAsync()
|
||||
{
|
||||
if (_contentByName.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var contentType = new ContentTypeBuilder()
|
||||
.WithAlias("theContentType")
|
||||
.Build();
|
||||
contentType.AllowedAsRoot = true;
|
||||
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
|
||||
contentType.AllowedContentTypes = [new() { Alias = contentType.Alias, Key = contentType.Key }];
|
||||
await ContentTypeService.UpdateAsync(contentType, Constants.Security.SuperUserKey);
|
||||
|
||||
foreach (var rootNumber in Enumerable.Range(1, 5))
|
||||
{
|
||||
var root = new ContentBuilder()
|
||||
.WithContentType(contentType)
|
||||
.WithName($"{rootNumber}")
|
||||
.Build();
|
||||
ContentService.Save(root);
|
||||
_contentByName[root.Name!] = root;
|
||||
|
||||
foreach (var childNumber in Enumerable.Range(1, 10))
|
||||
{
|
||||
var child = new ContentBuilder()
|
||||
.WithContentType(contentType)
|
||||
.WithParent(root)
|
||||
.WithName($"{rootNumber}-{childNumber}")
|
||||
.Build();
|
||||
ContentService.Save(child);
|
||||
_contentByName[child.Name!] = child;
|
||||
|
||||
foreach (var grandChildNumber in Enumerable.Range(1, 5))
|
||||
{
|
||||
var grandchild = new ContentBuilder()
|
||||
.WithContentType(contentType)
|
||||
.WithParent(child)
|
||||
.WithName($"{rootNumber}-{childNumber}-{grandChildNumber}")
|
||||
.Build();
|
||||
ContentService.Save(grandchild);
|
||||
_contentByName[grandchild.Name!] = grandchild;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_userGroup = new UserGroupBuilder()
|
||||
.WithAlias("theGroup")
|
||||
.WithAllowedSections(["content"])
|
||||
.Build();
|
||||
_userGroup.StartContentId = null;
|
||||
await UserGroupService.CreateAsync(_userGroup, Constants.Security.SuperUserKey);
|
||||
}
|
||||
|
||||
private async Task<string[]> CreateUserAndGetStartNodePaths(params int[] startNodeIds)
|
||||
{
|
||||
var user = await CreateUser(startNodeIds);
|
||||
|
||||
var contentStartNodePaths = user.GetContentStartNodePaths(EntityService, AppCaches.NoCache);
|
||||
Assert.IsNotNull(contentStartNodePaths);
|
||||
|
||||
return contentStartNodePaths;
|
||||
}
|
||||
|
||||
private async Task<int[]> CreateUserAndGetStartNodeIds(params int[] startNodeIds)
|
||||
{
|
||||
var user = await CreateUser(startNodeIds);
|
||||
|
||||
var contentStartNodeIds = user.CalculateContentStartNodeIds(EntityService, AppCaches.NoCache);
|
||||
Assert.IsNotNull(contentStartNodeIds);
|
||||
|
||||
return contentStartNodeIds;
|
||||
}
|
||||
|
||||
private async Task<User> CreateUser(int[] startNodeIds)
|
||||
{
|
||||
var user = 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);
|
||||
|
||||
Assert.IsTrue(attempt.Success);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<EnablePackageValidation>$(BaseEnablePackageValidation)</EnablePackageValidation>
|
||||
<NoWarn>$(NoWarn),NU5100</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
TODO: Fix and remove overrides:
|
||||
@@ -26,7 +26,7 @@
|
||||
-->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors),CS0108,SYSLIB0012,CS0618,SA1116,SA1117,CS0162,CS0169,SA1134,SA1405,CS4014,CS1998,CS0649,CS0168</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
@@ -253,5 +253,17 @@
|
||||
<Compile Update="Umbraco.Core\Services\PublishedUrlInfoProviderTests.cs">
|
||||
<DependentUpon>PublishedUrlInfoProviderTestsBase.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="ManagementApi\Services\UserStartNodeEntitiesServiceTests.ChildUserAccessEntities.cs">
|
||||
<DependentUpon>UserStartNodeEntitiesServiceTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="ManagementApi\Services\UserStartNodeEntitiesServiceTests.RootUserAccessEntities.cs">
|
||||
<DependentUpon>UserStartNodeEntitiesServiceTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="ManagementApi\Services\UserStartNodeEntitiesServiceMediaTests.childUserAccessEntities.cs">
|
||||
<DependentUpon>UserStartNodeEntitiesServiceMediaTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="ManagementApi\Services\UserStartNodeEntitiesServiceMediaTests.RootUserAccessEntities.cs">
|
||||
<DependentUpon>UserStartNodeEntitiesServiceMediaTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user