Added user start node restrictions to sibling endpoints (#19839)

* Added user start node restrictions to sibling endpoints.

* Further integration tests.

* Tidy up.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Revert previous update.

* Applied previous update correctly.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Andy Butland
2025-08-05 09:53:39 +02:00
committed by GitHub
parent f5ff2bbf59
commit 20254f0bbc
17 changed files with 492 additions and 51 deletions

View File

@@ -0,0 +1,131 @@
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services;
public partial class UserStartNodeEntitiesServiceMediaTests
{
[Test]
public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParent_YieldsAll_AsAllowed()
{
var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1"].Id);
var siblings = UserStartNodeEntitiesService
.SiblingUserAccessEntities(
UmbracoObjectTypes.Media,
mediaStartNodePaths,
_mediaByName["1-5"].Key,
2,
2,
BySortOrder)
.ToArray();
Assert.AreEqual(5, siblings.Length);
Assert.Multiple(() =>
{
for (int i = 0; i < 4; i++)
{
Assert.AreEqual(_mediaByName[$"1-{i + 3}"].Key, siblings[i].Entity.Key);
Assert.IsTrue(siblings[i].HasAccess);
}
});
}
[Test]
public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParentAndTarget_YieldsOnlyTarget_AsAllowed()
{
// See notes on ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOnlyGrandchild.
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1"].Id, _mediaByName["1-5"].Id);
var siblings = UserStartNodeEntitiesService
.SiblingUserAccessEntities(
UmbracoObjectTypes.Media,
contentStartNodePaths,
_mediaByName["1-5"].Key,
2,
2,
BySortOrder)
.ToArray();
Assert.AreEqual(1, siblings.Length);
Assert.Multiple(() =>
{
Assert.AreEqual(_mediaByName[$"1-5"].Key, siblings[0].Entity.Key);
Assert.IsTrue(siblings[0].HasAccess);
});
}
[Test]
public async Task SiblingUserAccessEntities_WithStartNodeOfTarget_YieldsOnlyTarget_AsAllowed()
{
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-5"].Id);
var siblings = UserStartNodeEntitiesService
.SiblingUserAccessEntities(
UmbracoObjectTypes.Media,
contentStartNodePaths,
_mediaByName["1-5"].Key,
2,
2,
BySortOrder)
.ToArray();
Assert.AreEqual(1, siblings.Length);
Assert.Multiple(() =>
{
Assert.AreEqual(_mediaByName[$"1-5"].Key, siblings[0].Entity.Key);
Assert.IsTrue(siblings[0].HasAccess);
});
}
[Test]
public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetAndSiblings_YieldsOnlyPermitted_AsAllowed()
{
var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-3"].Id, _mediaByName["1-5"].Id, _mediaByName["1-7"].Id);
var siblings = UserStartNodeEntitiesService
.SiblingUserAccessEntities(
UmbracoObjectTypes.Media,
mediaStartNodePaths,
_mediaByName["1-5"].Key,
2,
2,
BySortOrder)
.ToArray();
Assert.AreEqual(3, siblings.Length);
Assert.Multiple(() =>
{
Assert.AreEqual(_mediaByName[$"1-3"].Key, siblings[0].Entity.Key);
Assert.IsTrue(siblings[0].HasAccess);
Assert.AreEqual(_mediaByName[$"1-5"].Key, siblings[1].Entity.Key);
Assert.IsTrue(siblings[1].HasAccess);
Assert.AreEqual(_mediaByName[$"1-7"].Key, siblings[2].Entity.Key);
Assert.IsTrue(siblings[2].HasAccess);
});
}
[Test]
public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetGrandchild_YieldsTarget_AsNotAllowed()
{
var mediaStartNodePaths = await CreateUserAndGetStartNodePaths(_mediaByName["1-5-1"].Id);
var siblings = UserStartNodeEntitiesService
.SiblingUserAccessEntities(
UmbracoObjectTypes.Media,
mediaStartNodePaths,
_mediaByName["1-5"].Key,
2,
2,
BySortOrder)
.ToArray();
Assert.AreEqual(1, siblings.Length);
Assert.Multiple(() =>
{
Assert.AreEqual(_mediaByName[$"1-5"].Key, siblings[0].Entity.Key);
Assert.IsFalse(siblings[0].HasAccess);
});
}
}

View File

@@ -1,4 +1,4 @@
using NUnit.Framework;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services;

View File

@@ -0,0 +1,131 @@
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services;
public partial class UserStartNodeEntitiesServiceTests
{
[Test]
public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParent_YieldsAll_AsAllowed()
{
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1"].Id);
var siblings = UserStartNodeEntitiesService
.SiblingUserAccessEntities(
UmbracoObjectTypes.Document,
contentStartNodePaths,
_contentByName["1-5"].Key,
2,
2,
BySortOrder)
.ToArray();
Assert.AreEqual(5, siblings.Length);
Assert.Multiple(() =>
{
for (int i = 0; i < 4; i++)
{
Assert.AreEqual(_contentByName[$"1-{i + 3}"].Key, siblings[i].Entity.Key);
Assert.IsTrue(siblings[i].HasAccess);
}
});
}
[Test]
public async Task SiblingUserAccessEntities_WithStartNodeOfTargetParentAndTarget_YieldsOnlyTarget_AsAllowed()
{
// See notes on ChildUserAccessEntities_ChildAndGrandchildAsStartNode_AllowsOnlyGrandchild.
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1"].Id, _contentByName["1-5"].Id);
var siblings = UserStartNodeEntitiesService
.SiblingUserAccessEntities(
UmbracoObjectTypes.Document,
contentStartNodePaths,
_contentByName["1-5"].Key,
2,
2,
BySortOrder)
.ToArray();
Assert.AreEqual(1, siblings.Length);
Assert.Multiple(() =>
{
Assert.AreEqual(_contentByName[$"1-5"].Key, siblings[0].Entity.Key);
Assert.IsTrue(siblings[0].HasAccess);
});
}
[Test]
public async Task SiblingUserAccessEntities_WithStartNodeOfTarget_YieldsOnlyTarget_AsAllowed()
{
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-5"].Id);
var siblings = UserStartNodeEntitiesService
.SiblingUserAccessEntities(
UmbracoObjectTypes.Document,
contentStartNodePaths,
_contentByName["1-5"].Key,
2,
2,
BySortOrder)
.ToArray();
Assert.AreEqual(1, siblings.Length);
Assert.Multiple(() =>
{
Assert.AreEqual(_contentByName[$"1-5"].Key, siblings[0].Entity.Key);
Assert.IsTrue(siblings[0].HasAccess);
});
}
[Test]
public async Task SiblingUserAccessEntities_WithStartsNodesOfTargetAndSiblings_YieldsOnlyPermitted_AsAllowed()
{
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-3"].Id, _contentByName["1-5"].Id, _contentByName["1-7"].Id);
var siblings = UserStartNodeEntitiesService
.SiblingUserAccessEntities(
UmbracoObjectTypes.Document,
contentStartNodePaths,
_contentByName["1-5"].Key,
2,
2,
BySortOrder)
.ToArray();
Assert.AreEqual(3, siblings.Length);
Assert.Multiple(() =>
{
Assert.AreEqual(_contentByName[$"1-3"].Key, siblings[0].Entity.Key);
Assert.IsTrue(siblings[0].HasAccess);
Assert.AreEqual(_contentByName[$"1-5"].Key, siblings[1].Entity.Key);
Assert.IsTrue(siblings[1].HasAccess);
Assert.AreEqual(_contentByName[$"1-7"].Key, siblings[2].Entity.Key);
Assert.IsTrue(siblings[2].HasAccess);
});
}
[Test]
public async Task SiblingUserAccessEntities_WithStartNodesOfTargetChild_YieldsTarget_AsNotAllowed()
{
var contentStartNodePaths = await CreateUserAndGetStartNodePaths(_contentByName["1-5-1"].Id);
var siblings = UserStartNodeEntitiesService
.SiblingUserAccessEntities(
UmbracoObjectTypes.Document,
contentStartNodePaths,
_contentByName["1-5"].Key,
2,
2,
BySortOrder)
.ToArray();
Assert.AreEqual(1, siblings.Length);
Assert.Multiple(() =>
{
Assert.AreEqual(_contentByName[$"1-5"].Key, siblings[0].Entity.Key);
Assert.IsFalse(siblings[0].HasAccess);
});
}
}

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Core;
@@ -32,7 +32,7 @@ public partial class UserStartNodeEntitiesServiceTests : UmbracoIntegrationTest
private IUserStartNodeEntitiesService UserStartNodeEntitiesService => GetRequiredService<IUserStartNodeEntitiesService>();
protected readonly Ordering BySortOrder = Ordering.By("sortOrder");
protected static readonly Ordering BySortOrder = Ordering.By("sortOrder");
protected override void ConfigureTestServices(IServiceCollection services)
{

View File

@@ -5,6 +5,7 @@ using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.ContentTypeEditing;
using Umbraco.Cms.Infrastructure.Persistence;
@@ -936,9 +937,9 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
{
var children = CreateSiblingsTestData();
var taget = children[1];
var target = children[1];
var result = EntityService.GetSiblings(taget.Key, UmbracoObjectTypes.Document, 1, 1).ToArray();
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 1).ToArray();
Assert.AreEqual(3, result.Length);
Assert.IsTrue(result[0].Key == children[0].Key);
Assert.IsTrue(result[1].Key == children[1].Key);
@@ -953,8 +954,8 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
var trash = children[1];
ContentService.MoveToRecycleBin(trash);
var taget = children[2];
var result = EntityService.GetSiblings(taget.Key, UmbracoObjectTypes.Document, 1, 1).ToArray();
var target = children[2];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 1).ToArray();
Assert.AreEqual(3, result.Length);
Assert.IsFalse(result.Any(x => x.Key == trash.Key));
Assert.IsTrue(result[0].Key == children[0].Key);
@@ -962,6 +963,44 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
Assert.IsTrue(result[2].Key == children[3].Key);
}
[Test]
public void EntityService_Siblings_SkipsFilteredEntities_UsingFilterWithSet()
{
var children = CreateSiblingsTestData();
// Apply a filter that excludes the child at index 1. We'd expect to not get this, but
// get still get one previous sibling, i.e. the entity at index 0.
Guid[] keysToExclude = [children[1].Key];
IQuery<IUmbracoEntity> filter = ScopeProvider.CreateQuery<IUmbracoEntity>().Where(x => !keysToExclude.Contains(x.Key));
var target = children[2];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 1, filter).ToArray();
Assert.AreEqual(3, result.Length);
Assert.IsFalse(result.Any(x => x.Key == keysToExclude[0]));
Assert.IsTrue(result[0].Key == children[0].Key);
Assert.IsTrue(result[1].Key == children[2].Key);
Assert.IsTrue(result[2].Key == children[3].Key);
}
[Test]
public void EntityService_Siblings_SkipsFilteredEntities_UsingFilterWithoutSet()
{
var children = CreateSiblingsTestData();
// Apply a filter that excludes the child at index 1. We'd expect to not get this, but
// get still get one previous sibling, i.e. the entity at index 0.
var keyToExclude = children[1].Key;
IQuery<IUmbracoEntity> filter = ScopeProvider.CreateQuery<IUmbracoEntity>().Where(x => x.Key != keyToExclude);
var target = children[2];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 1, filter).ToArray();
Assert.AreEqual(3, result.Length);
Assert.IsFalse(result.Any(x => x.Key == keyToExclude));
Assert.IsTrue(result[0].Key == children[0].Key);
Assert.IsTrue(result[1].Key == children[2].Key);
Assert.IsTrue(result[2].Key == children[3].Key);
}
[Test]
public void EntityService_Siblings_RespectsOrdering()
{
@@ -970,8 +1009,8 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
// Order the children by name to ensure the ordering works when differing from the default sort order, the name is a GUID.
children = children.OrderBy(x => x.Name).ToList();
var taget = children[1];
var result = EntityService.GetSiblings(taget.Key, UmbracoObjectTypes.Document, 1, 1, Ordering.By(nameof(NodeDto.Text))).ToArray();
var target = children[1];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 1, ordering: Ordering.By(nameof(NodeDto.Text))).ToArray();
Assert.AreEqual(3, result.Length);
Assert.IsTrue(result[0].Key == children[0].Key);
Assert.IsTrue(result[1].Key == children[1].Key);
@@ -983,8 +1022,8 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
{
var children = CreateSiblingsTestData();
var taget = children[1];
var result = EntityService.GetSiblings(taget.Key, UmbracoObjectTypes.Document, 100, 1).ToArray();
var target = children[1];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 100, 1).ToArray();
Assert.AreEqual(3, result.Length);
Assert.IsTrue(result[0].Key == children[0].Key);
Assert.IsTrue(result[1].Key == children[1].Key);
@@ -996,8 +1035,8 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest
{
var children = CreateSiblingsTestData();
var taget = children[^2];
var result = EntityService.GetSiblings(taget.Key, UmbracoObjectTypes.Document, 1, 100).ToArray();
var target = children[^2];
var result = EntityService.GetSiblings(target.Key, UmbracoObjectTypes.Document, 1, 100).ToArray();
Assert.AreEqual(3, result.Length);
Assert.IsTrue(result[^1].Key == children[^1].Key);
Assert.IsTrue(result[^2].Key == children[^2].Key);

View File

@@ -286,12 +286,18 @@
<Compile Update="ManagementApi\Services\UserStartNodeEntitiesServiceTests.RootUserAccessEntities.cs">
<DependentUpon>UserStartNodeEntitiesServiceTests.cs</DependentUpon>
</Compile>
<Compile Update="ManagementApi\Services\UserStartNodeEntitiesServiceTests.SiblingUserAccessEntities.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>
<Compile Update="ManagementApi\Services\UserStartNodeEntitiesServiceMediaTests.SiblingUserAccessEntities.cs">
<DependentUpon>UserStartNodeEntitiesServiceMediaTests.cs</DependentUpon>
</Compile>
<Compile Update="Umbraco.Infrastructure\Services\ContentBlueprintEditingServiceTests.GetScaffold.cs">
<DependentUpon>ContentBlueprintEditingServiceTests.cs</DependentUpon>
</Compile>