Skip to content

Commit 20254f0

Browse files
AndyButlandCopilot
andauthored
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 <[email protected]> * Revert previous update. * Applied previous update correctly. --------- Co-authored-by: Copilot <[email protected]>
1 parent f5ff2bb commit 20254f0

17 files changed

+492
-51
lines changed

src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/SiblingsDocumentTreeController.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public SiblingsDocumentTreeController(
3535
[HttpGet("siblings")]
3636
[MapToApiVersion("1.0")]
3737
[ProducesResponseType(typeof(IEnumerable<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
38-
public Task<ActionResult<IEnumerable<DocumentTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
39-
=> GetSiblings(target, before, after);
38+
public Task<ActionResult<IEnumerable<DocumentTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
39+
{
40+
IgnoreUserStartNodesForDataType(dataTypeId);
41+
return GetSiblings(target, before, after);
42+
}
4043
}

src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/SiblingsMediaTreeController.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public SiblingsMediaTreeController(
2424

2525
[HttpGet("siblings")]
2626
[ProducesResponseType(typeof(IEnumerable<MediaTreeItemResponseModel>), StatusCodes.Status200OK)]
27-
public Task<ActionResult<IEnumerable<MediaTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after)
28-
=> GetSiblings(target, before, after);
27+
public Task<ActionResult<IEnumerable<MediaTreeItemResponseModel>>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null)
28+
{
29+
IgnoreUserStartNodesForDataType(dataTypeId);
30+
return GetSiblings(target, before, after);
31+
}
2932
}

src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.AspNetCore.Mvc;
1+
using Microsoft.AspNetCore.Mvc;
22
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
33
using Umbraco.Cms.Api.Management.ViewModels;
44
using Umbraco.Cms.Api.Management.ViewModels.Tree;
@@ -46,7 +46,7 @@ protected Task<ActionResult<PagedViewModel<TItem>>> GetChildren(Guid parentId, i
4646

4747
protected Task<ActionResult<IEnumerable<TItem>>> GetSiblings(Guid target, int before, int after)
4848
{
49-
IEntitySlim[] siblings = EntityService.GetSiblings(target, ItemObjectType, before, after, ItemOrdering).ToArray();
49+
IEntitySlim[] siblings = GetSiblingEntities(target, before, after);
5050
if (siblings.Length == 0)
5151
{
5252
return Task.FromResult<ActionResult<IEnumerable<TItem>>>(NotFound());
@@ -110,7 +110,8 @@ protected virtual IEntitySlim[] GetPagedRootEntities(int skip, int take, out lon
110110
.ToArray();
111111

112112
protected virtual IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip, int take, out long totalItems) =>
113-
EntityService.GetPagedChildren(
113+
EntityService
114+
.GetPagedChildren(
114115
parentKey,
115116
ItemObjectType,
116117
skip,
@@ -119,6 +120,16 @@ protected virtual IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip,
119120
ordering: ItemOrdering)
120121
.ToArray();
121122

123+
protected virtual IEntitySlim[] GetSiblingEntities(Guid target, int before, int after) =>
124+
EntityService
125+
.GetSiblings(
126+
target,
127+
ItemObjectType,
128+
before,
129+
after,
130+
ordering: ItemOrdering)
131+
.ToArray();
132+
122133
protected virtual TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
123134
=> entities.Select(entity => MapTreeItemViewModel(parentKey, entity)).ToArray();
124135

src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Umbraco.Cms.Api.Management.Models.Entities;
1+
using Umbraco.Cms.Api.Management.Models.Entities;
22
using Umbraco.Cms.Api.Management.Services.Entities;
33
using Umbraco.Cms.Api.Management.ViewModels.Tree;
44
using Umbraco.Cms.Core;
@@ -59,6 +59,24 @@ protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, int skip,
5959
return CalculateAccessMap(() => userAccessEntities, out _);
6060
}
6161

62+
protected override IEntitySlim[] GetSiblingEntities(Guid target, int before, int after)
63+
{
64+
if (UserHasRootAccess() || IgnoreUserStartNodes())
65+
{
66+
return base.GetSiblingEntities(target, before, after);
67+
}
68+
69+
IEnumerable<UserAccessEntity> userAccessEntities = _userStartNodeEntitiesService.SiblingUserAccessEntities(
70+
ItemObjectType,
71+
UserStartNodePaths,
72+
target,
73+
before,
74+
after,
75+
ItemOrdering);
76+
77+
return CalculateAccessMap(() => userAccessEntities, out _);
78+
}
79+
6280
protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
6381
{
6482
if (UserHasRootAccess() || IgnoreUserStartNodes())

src/Umbraco.Cms.Api.Management/Services/Entities/IUserStartNodeEntitiesService.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Umbraco.Cms.Core.Models;
1+
using Umbraco.Cms.Core.Models;
22
using Umbraco.Cms.Core.Models.Entities;
33
using Umbraco.Cms.Core.Services;
44
using Umbraco.Cms.Api.Management.Models.Entities;
@@ -64,6 +64,28 @@ IEnumerable<UserAccessEntity> ChildUserAccessEntities(
6464
/// </remarks>
6565
IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> candidateChildren, string[] userStartNodePaths);
6666

67+
/// <summary>
68+
/// Calculates the applicable sibling entities for a given object type for users without root access.
69+
/// </summary>
70+
/// <param name="umbracoObjectType">The object type.</param>
71+
/// <param name="userStartNodePaths">The calculated start node paths for the user.</param>
72+
/// <param name="targetKey">The key of the target.</param>
73+
/// <param name="before">The number of applicable siblings to retrieve before the target.</param>
74+
/// <param name="after">The number of applicable siblings to retrieve after the target.</param>
75+
/// <param name="ordering">The ordering to apply when fetching and paginating the children.</param>
76+
/// <returns>A list of sibling entities applicable for the user.</returns>
77+
/// <remarks>
78+
/// The returned entities may include entities that outside of the user start node scope, but are needed to
79+
/// for browsing to the actual user start nodes. These entities will be marked as "no access" entities.
80+
/// </remarks>
81+
IEnumerable<UserAccessEntity> SiblingUserAccessEntities(
82+
UmbracoObjectTypes umbracoObjectType,
83+
string[] userStartNodePaths,
84+
Guid targetKey,
85+
int before,
86+
int after,
87+
Ordering ordering) => [];
88+
6789
/// <summary>
6890
/// Calculates the access level of a collection of entities for users without root access.
6991
/// </summary>

src/Umbraco.Cms.Api.Management/Services/Entities/UserStartNodeEntitiesService.cs

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.Extensions.DependencyInjection;
1+
using Microsoft.Extensions.DependencyInjection;
22
using Umbraco.Cms.Core;
33
using Umbraco.Cms.Core.Models;
44
using Umbraco.Cms.Core.Models.Entities;
@@ -36,17 +36,17 @@ public UserStartNodeEntitiesService(IEntityService entityService, ICoreScopeProv
3636
/// <inheritdoc />
3737
public IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds)
3838
{
39-
// root entities for users without root access should include:
39+
// Root entities for users without root access should include:
4040
// - the start nodes that are actual root entities (level == 1)
4141
// - 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")
4242
IEntitySlim[] userStartEntities = userStartNodeIds.Any()
4343
? _entityService.GetAll(umbracoObjectType, userStartNodeIds).ToArray()
4444
: Array.Empty<IEntitySlim>();
4545

46-
// find the start nodes that are at root level (level == 1)
46+
// Find the start nodes that are at root level (level == 1).
4747
IEntitySlim[] allowedTopmostEntities = userStartEntities.Where(entity => entity.Level == 1).ToArray();
4848

49-
// find the root level ancestors of the rest of the start nodes, and add those as well
49+
// Find the root level ancestors of the rest of the start nodes, and add those as well.
5050
var nonAllowedTopmostEntityIds = userStartEntities.Except(allowedTopmostEntities)
5151
.Select(entity => int.TryParse(entity.Path.Split(Constants.CharArrays.Comma).Skip(1).FirstOrDefault(), out var id) ? id : 0)
5252
.Where(id => id > 0)
@@ -63,6 +63,7 @@ public IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes u
6363
.ToArray();
6464
}
6565

66+
/// <inheritdoc/>
6667
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(UmbracoObjectTypes umbracoObjectType, string[] userStartNodePaths, Guid parentKey, int skip, int take, Ordering ordering, out long totalItems)
6768
{
6869
Attempt<int> parentIdAttempt = _idKeyMap.GetIdForKey(parentKey, umbracoObjectType);
@@ -83,40 +84,46 @@ public IEnumerable<UserAccessEntity> ChildUserAccessEntities(UmbracoObjectTypes
8384
IEntitySlim[] children;
8485
if (userStartNodePaths.Any(path => $"{parent.Path},".StartsWith($"{path},")))
8586
{
86-
// the requested parent is one of the user start nodes (or a descendant of one), all children are by definition allowed
87+
// The requested parent is one of the user start nodes (or a descendant of one), all children are by definition allowed.
8788
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, ordering: ordering).ToArray();
8889
return ChildUserAccessEntities(children, userStartNodePaths);
8990
}
9091

91-
// 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
92-
// - 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.
93-
var userStartNodePathIds = userStartNodePaths.Select(path => path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray();
94-
var allowedChildIds = userStartNodePathIds
95-
.Where(ids => ids.Contains(parentId))
96-
// given the previous checks, the parent ID can never be the last in the user start node path, so this is safe
97-
.Select(ids => ids[ids.IndexOf(parentId) + 1])
98-
.Distinct()
99-
.ToArray();
92+
int[] allowedChildIds = GetAllowedIds(userStartNodePaths, parentId);
10093

10194
totalItems = allowedChildIds.Length;
10295
if (allowedChildIds.Length == 0)
10396
{
104-
// the requested parent is outside the scope of any user start nodes
97+
// The requested parent is outside the scope of any user start nodes.
10598
return [];
10699
}
107100

108-
// even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children
101+
// Even though we know the IDs of the allowed child entities to fetch, we still use a Query to yield correctly sorted children.
109102
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedChildIds.Contains(x.Id));
110103
children = _entityService.GetPagedChildren(parentKey, umbracoObjectType, skip, take, out totalItems, query, ordering).ToArray();
111104
return ChildUserAccessEntities(children, userStartNodePaths);
112105
}
113106

107+
private static int[] GetAllowedIds(string[] userStartNodePaths, int parentId)
108+
{
109+
// 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
110+
// that are the final entries in the path.
111+
// 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.
112+
var userStartNodePathIds = userStartNodePaths.Select(path => path.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray()).ToArray();
113+
return userStartNodePathIds
114+
.Where(ids => ids.Contains(parentId))
115+
.Select(ids => ids[ids.IndexOf(parentId) + 1]) // Given the previous checks, the parent ID can never be the last in the user start node path, so this is safe
116+
.Distinct()
117+
.ToArray();
118+
}
119+
114120
/// <inheritdoc />
115121
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> candidateChildren, string[] userStartNodePaths)
116-
// child entities for users without root access should include:
122+
123+
// Child or sibling entities for users without root access should include:
117124
// - children that are descendant-or-self of a user start node
118125
// - children that are ancestors of a user start node (required for browsing to the actual start nodes - will be marked as "no access")
119-
// all other candidate children should be discarded
126+
// All other candidate children should be discarded.
120127
=> candidateChildren.Select(child =>
121128
{
122129
// is descendant-or-self of a start node?
@@ -134,9 +141,55 @@ public IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntity
134141
return null;
135142
}).WhereNotNull().ToArray();
136143

144+
/// <inheritdoc />
145+
public IEnumerable<UserAccessEntity> SiblingUserAccessEntities(UmbracoObjectTypes umbracoObjectType, string[] userStartNodePaths, Guid targetKey, int before, int after, Ordering ordering)
146+
{
147+
Attempt<int> targetIdAttempt = _idKeyMap.GetIdForKey(targetKey, umbracoObjectType);
148+
if (targetIdAttempt.Success is false)
149+
{
150+
return [];
151+
}
152+
153+
var targetId = targetIdAttempt.Result;
154+
IEntitySlim? target = _entityService.Get(targetId);
155+
if (target is null)
156+
{
157+
return [];
158+
}
159+
160+
IEntitySlim[] siblings;
161+
162+
IEntitySlim? targetParent = _entityService.Get(target.ParentId);
163+
if (targetParent is null) // Even if the parent is the root, we still expect to get a value here.
164+
{
165+
return [];
166+
}
167+
168+
if (userStartNodePaths.Any(path => $"{targetParent?.Path},".StartsWith($"{path},")))
169+
{
170+
// The requested parent of the target is one of the user start nodes (or a descendant of one), all siblings are by definition allowed.
171+
siblings = _entityService.GetSiblings(targetKey, umbracoObjectType, before, after, ordering: ordering).ToArray();
172+
return ChildUserAccessEntities(siblings, userStartNodePaths);
173+
}
174+
175+
int[] allowedSiblingIds = GetAllowedIds(userStartNodePaths, targetParent.Id);
176+
177+
if (allowedSiblingIds.Length == 0)
178+
{
179+
// The requested target is outside the scope of any user start nodes.
180+
return [];
181+
}
182+
183+
// Even though we know the IDs of the allowed sibling entities to fetch, we still use a Query to yield correctly sorted children.
184+
IQuery<IUmbracoEntity> query = _scopeProvider.CreateQuery<IUmbracoEntity>().Where(x => allowedSiblingIds.Contains(x.Id));
185+
siblings = _entityService.GetSiblings(targetKey, umbracoObjectType, before, after, query, ordering).ToArray();
186+
return ChildUserAccessEntities(siblings, userStartNodePaths);
187+
}
188+
137189
/// <inheritdoc />
138190
public IEnumerable<UserAccessEntity> UserAccessEntities(IEnumerable<IEntitySlim> entities, string[] userStartNodePaths)
139-
// entities for users without root access should include:
191+
192+
// Entities for users without root access should include:
140193
// - entities that are descendant-or-self of a user start node as regular entities
141194
// - all other entities as "no access" entities
142195
=> entities.Select(entity => new UserAccessEntity(entity, IsDescendantOrSelf(entity, userStartNodePaths))).ToArray();

src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ public interface IEntityRepository : IRepository
2626
/// <param name="targetKey">The key of the target entity whose siblings are to be retrieved.</param>
2727
/// <param name="before">The number of siblings to retrieve before the target entity.</param>
2828
/// <param name="after">The number of siblings to retrieve after the target entity.</param>
29+
/// <param name="filter">An optional filter to apply to the result set.</param>
2930
/// <param name="ordering">The ordering to apply to the siblings.</param>
3031
/// <returns>Enumerable of sibling entities.</returns>
31-
IEnumerable<IEntitySlim> GetSiblings(Guid objectType, Guid targetKey, int before, int after, Ordering ordering) => [];
32+
IEnumerable<IEntitySlim> GetSiblings(Guid objectType, Guid targetKey, int before, int after, IQuery<IUmbracoEntity>? filter, Ordering ordering) => [];
3233

3334
/// <summary>
3435
/// Gets entities for a query

src/Umbraco.Core/Services/DateTypeServiceExtensions.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Umbraco.Cms.Core.Models;
1+
using Umbraco.Cms.Core.Models;
22
using Umbraco.Cms.Core.PropertyEditors;
33
using Umbraco.Cms.Core.Services;
44

@@ -8,11 +8,6 @@ public static class DateTypeServiceExtensions
88
{
99
public static bool IsDataTypeIgnoringUserStartNodes(this IDataTypeService dataTypeService, Guid key)
1010
{
11-
if (DataTypeExtensions.IsBuildInDataType(key))
12-
{
13-
return false; // built in ones can never be ignoring start nodes
14-
}
15-
1611
IDataType? dataType = dataTypeService.GetAsync(key).GetAwaiter().GetResult();
1712

1813
if (dataType != null && dataType.ConfigurationObject is IIgnoreUserStartNodesConfig ignoreStartNodesConfig)

src/Umbraco.Core/Services/EntityService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ public IEnumerable<IEntitySlim> GetSiblings(
324324
UmbracoObjectTypes objectType,
325325
int before,
326326
int after,
327+
IQuery<IUmbracoEntity>? filter = null,
327328
Ordering? ordering = null)
328329
{
329330
if (before < 0)
@@ -345,6 +346,7 @@ public IEnumerable<IEntitySlim> GetSiblings(
345346
key,
346347
before,
347348
after,
349+
filter,
348350
ordering);
349351

350352
scope.Complete();

src/Umbraco.Core/Services/IEntityService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,15 @@ IEnumerable<IEntitySlim> GetAll<T>(params Guid[] keys)
177177
/// <param name="objectType">The object type key of the entities.</param>
178178
/// <param name="before">The number of siblings to retrieve before the target entity. Needs to be greater or equal to 0.</param>
179179
/// <param name="after">The number of siblings to retrieve after the target entity. Needs to be greater or equal to 0.</param>
180+
/// <param name="filter">An optional filter to apply to the result set.</param>
180181
/// <param name="ordering">The ordering to apply to the siblings.</param>
181182
/// <returns>Enumerable of sibling entities.</returns>
182183
IEnumerable<IEntitySlim> GetSiblings(
183184
Guid key,
184185
UmbracoObjectTypes objectType,
185186
int before,
186187
int after,
188+
IQuery<IUmbracoEntity>? filter = null,
187189
Ordering? ordering = null) => [];
188190

189191
/// <summary>

0 commit comments

Comments
 (0)