Skip to content

Commit a0e3ca6

Browse files
authored
Aggregate document permissions for current user in API response (#18721)
* Create integration test verifying existing behaviour. * Aggregate permissions per document for the current user response. * Refactoring following Codescene warnings.
1 parent 1a9d8e3 commit a0e3ca6

File tree

4 files changed

+280
-17
lines changed

4 files changed

+280
-17
lines changed

src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
using Umbraco.Cms.Api.Management.Routing;
1+
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.Options;
3+
using Umbraco.Cms.Api.Management.Routing;
34
using Umbraco.Cms.Api.Management.Security;
45
using Umbraco.Cms.Api.Management.ViewModels;
56
using Umbraco.Cms.Api.Management.ViewModels.User;
67
using Umbraco.Cms.Api.Management.ViewModels.User.Current;
7-
using Umbraco.Cms.Core;
88
using Umbraco.Cms.Api.Management.ViewModels.User.Item;
9+
using Umbraco.Cms.Api.Management.ViewModels.UserGroup;
10+
using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions;
11+
using Umbraco.Cms.Core;
912
using Umbraco.Cms.Core.Cache;
1013
using Umbraco.Cms.Core.Configuration.Models;
14+
using Umbraco.Cms.Core.DependencyInjection;
1115
using Umbraco.Cms.Core.IO;
1216
using Umbraco.Cms.Core.Mail;
1317
using Umbraco.Cms.Core.Media;
@@ -20,7 +24,6 @@ namespace Umbraco.Cms.Api.Management.Factories;
2024

2125
public class UserPresentationFactory : IUserPresentationFactory
2226
{
23-
2427
private readonly IEntityService _entityService;
2528
private readonly AppCaches _appCaches;
2629
private readonly MediaFileManager _mediaFileManager;
@@ -31,7 +34,10 @@ public class UserPresentationFactory : IUserPresentationFactory
3134
private readonly IPasswordConfigurationPresentationFactory _passwordConfigurationPresentationFactory;
3235
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
3336
private readonly SecuritySettings _securitySettings;
37+
private readonly IUserService _userService;
38+
private readonly IContentService _contentService;
3439

40+
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
3541
public UserPresentationFactory(
3642
IEntityService entityService,
3743
AppCaches appCaches,
@@ -43,6 +49,35 @@ public UserPresentationFactory(
4349
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
4450
IOptionsSnapshot<SecuritySettings> securitySettings,
4551
IBackOfficeExternalLoginProviders externalLoginProviders)
52+
: this(
53+
entityService,
54+
appCaches,
55+
mediaFileManager,
56+
imageUrlGenerator,
57+
userGroupPresentationFactory,
58+
absoluteUrlBuilder,
59+
emailSender,
60+
passwordConfigurationPresentationFactory,
61+
securitySettings,
62+
externalLoginProviders,
63+
StaticServiceProvider.Instance.GetRequiredService<IUserService>(),
64+
StaticServiceProvider.Instance.GetRequiredService<IContentService>())
65+
{
66+
}
67+
68+
public UserPresentationFactory(
69+
IEntityService entityService,
70+
AppCaches appCaches,
71+
MediaFileManager mediaFileManager,
72+
IImageUrlGenerator imageUrlGenerator,
73+
IUserGroupPresentationFactory userGroupPresentationFactory,
74+
IAbsoluteUrlBuilder absoluteUrlBuilder,
75+
IEmailSender emailSender,
76+
IPasswordConfigurationPresentationFactory passwordConfigurationPresentationFactory,
77+
IOptionsSnapshot<SecuritySettings> securitySettings,
78+
IBackOfficeExternalLoginProviders externalLoginProviders,
79+
IUserService userService,
80+
IContentService contentService)
4681
{
4782
_entityService = entityService;
4883
_appCaches = appCaches;
@@ -54,6 +89,8 @@ public UserPresentationFactory(
5489
_externalLoginProviders = externalLoginProviders;
5590
_securitySettings = securitySettings.Value;
5691
_absoluteUrlBuilder = absoluteUrlBuilder;
92+
_userService = userService;
93+
_contentService = contentService;
5794
}
5895

5996
public UserResponseModel CreateResponseModel(IUser user)
@@ -195,7 +232,7 @@ public async Task<CurrentUserResponseModel> CreateCurrentUserResponseModelAsync(
195232
var contentStartNodeIds = user.CalculateContentStartNodeIds(_entityService, _appCaches);
196233
var documentStartNodeKeys = GetKeysFromIds(contentStartNodeIds, UmbracoObjectTypes.Document);
197234

198-
var permissions = presentationGroups.SelectMany(x => x.Permissions).ToHashSet();
235+
var permissions = GetAggregatedGranularPermissions(user, presentationGroups);
199236
var fallbackPermissions = presentationGroups.SelectMany(x => x.FallbackPermissions).ToHashSet();
200237

201238
var hasAccessToAllLanguages = presentationGroups.Any(x => x.HasAccessToAllLanguages);
@@ -225,6 +262,42 @@ public async Task<CurrentUserResponseModel> CreateCurrentUserResponseModelAsync(
225262
});
226263
}
227264

265+
private HashSet<IPermissionPresentationModel> GetAggregatedGranularPermissions(IUser user, IEnumerable<UserGroupResponseModel> presentationGroups)
266+
{
267+
var permissions = presentationGroups.SelectMany(x => x.Permissions).ToHashSet();
268+
269+
// The raw permission data consists of several permissions for each document. We want to aggregate this server-side so
270+
// we return one set of aggregate permissions per document that the client will use.
271+
272+
// Get the unique document keys that have granular permissions.
273+
IEnumerable<Guid> documentKeysWithGranularPermissions = permissions
274+
.Where(x => x is DocumentPermissionPresentationModel)
275+
.Cast<DocumentPermissionPresentationModel>()
276+
.Select(x => x.Document.Id)
277+
.Distinct();
278+
279+
var aggregatedPermissions = new HashSet<IPermissionPresentationModel>();
280+
foreach (Guid documentKey in documentKeysWithGranularPermissions)
281+
{
282+
// Retrieve the path of the document.
283+
var path = _contentService.GetById(documentKey)?.Path;
284+
if (string.IsNullOrEmpty(path))
285+
{
286+
continue;
287+
}
288+
289+
// With the path we can call the same logic as used server-side for authorizing access to resources.
290+
EntityPermissionSet permissionsForPath = _userService.GetPermissionsForPath(user, path);
291+
aggregatedPermissions.Add(new DocumentPermissionPresentationModel
292+
{
293+
Document = new ReferenceByIdModel(documentKey),
294+
Verbs = permissionsForPath.GetAllPermissions()
295+
});
296+
}
297+
298+
return aggregatedPermissions;
299+
}
300+
228301
public async Task<CalculatedUserStartNodesResponseModel> CreateCalculatedUserStartNodesResponseModelAsync(IUser user)
229302
{
230303
var mediaStartNodeIds = user.CalculateMediaStartNodeIds(_entityService, _appCaches);

tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ public override IUserGroup Build()
159159
Key = key,
160160
StartContentId = startContentId,
161161
StartMediaId = startMediaId,
162-
Permissions = _permissions
162+
Permissions = _permissions,
163163
};
164164

165165
BuildAllowedSections(userGroup);
@@ -169,7 +169,6 @@ public override IUserGroup Build()
169169
return userGroup;
170170
}
171171

172-
173172
private void BuildAllowedSections(UserGroup userGroup)
174173
{
175174
foreach (var section in _allowedSections)
@@ -204,6 +203,6 @@ public static UserGroup CreateUserGroup(
204203
.WithAlias(alias + suffix)
205204
.WithName(name + suffix)
206205
.WithPermissions(permissions ?? new[] { "A", "B", "C" }.ToHashSet())
207-
.WithAllowedSections(allowedSections ?? new[] { "content", "media" })
206+
.WithAllowedSections(allowedSections ?? ["content", "media"])
208207
.Build();
209208
}

tests/Umbraco.Tests.Integration/ManagementApi/Factories/UserGroupPresentationFactoryTests.cs

Lines changed: 10 additions & 10 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 NUnit.Framework;
33
using Umbraco.Cms.Api.Management.Factories;
44
using Umbraco.Cms.Api.Management.Mapping.Permissions;
@@ -34,15 +34,15 @@ protected override void ConfigureTestServices(IServiceCollection services)
3434
services.AddTransient<IUserGroupPresentationFactory, UserGroupPresentationFactory>();
3535
services.AddSingleton<IPermissionPresentationFactory, PermissionPresentationFactory>();
3636
services.AddSingleton<DocumentPermissionMapper>();
37-
services.AddSingleton<IPermissionMapper>(x=>x.GetRequiredService<DocumentPermissionMapper>());
38-
services.AddSingleton<IPermissionPresentationMapper>(x=>x.GetRequiredService<DocumentPermissionMapper>());
37+
services.AddSingleton<IPermissionMapper>(x => x.GetRequiredService<DocumentPermissionMapper>());
38+
services.AddSingleton<IPermissionPresentationMapper>(x => x.GetRequiredService<DocumentPermissionMapper>());
3939
}
4040

4141

4242
[Test]
4343
public async Task Can_Map_Create_Model_And_Create()
4444
{
45-
var updateModel = new CreateUserGroupRequestModel()
45+
var createModel = new CreateUserGroupRequestModel()
4646
{
4747
Alias = "testAlias",
4848
FallbackPermissions = new HashSet<string>(),
@@ -53,7 +53,7 @@ public async Task Can_Map_Create_Model_And_Create()
5353
Permissions = new HashSet<IPermissionPresentationModel>()
5454
};
5555

56-
var attempt = await UserGroupPresentationFactory.CreateAsync(updateModel);
56+
var attempt = await UserGroupPresentationFactory.CreateAsync(createModel);
5757
Assert.IsTrue(attempt.Success);
5858

5959
var userGroupCreateAttempt = await UserGroupService.CreateAsync(attempt.Result, Constants.Security.SuperUserKey);
@@ -71,7 +71,7 @@ public async Task Can_Map_Create_Model_And_Create()
7171
[Test]
7272
public async Task Cannot_Create_UserGroup_With_Unexisting_Document_Reference()
7373
{
74-
var updateModel = new CreateUserGroupRequestModel()
74+
var createModel = new CreateUserGroupRequestModel()
7575
{
7676
Alias = "testAlias",
7777
FallbackPermissions = new HashSet<string>(),
@@ -89,7 +89,7 @@ public async Task Cannot_Create_UserGroup_With_Unexisting_Document_Reference()
8989
}
9090
};
9191

92-
var attempt = await UserGroupPresentationFactory.CreateAsync(updateModel);
92+
var attempt = await UserGroupPresentationFactory.CreateAsync(createModel);
9393
Assert.IsTrue(attempt.Success);
9494

9595
var userGroupCreateAttempt = await UserGroupService.CreateAsync(attempt.Result, Constants.Security.SuperUserKey);
@@ -102,11 +102,11 @@ public async Task Cannot_Create_UserGroup_With_Unexisting_Document_Reference()
102102
}
103103

104104
[Test]
105-
public async Task Can_Create_Usergroup_With_Empty_Granluar_Permissions_For_Document()
105+
public async Task Can_Create_Usergroup_With_Empty_Granular_Permissions_For_Document()
106106
{
107107
var contentKey = await CreateContent();
108108

109-
var updateModel = new CreateUserGroupRequestModel()
109+
var createModel = new CreateUserGroupRequestModel()
110110
{
111111
Alias = "testAlias",
112112
FallbackPermissions = new HashSet<string>(),
@@ -124,7 +124,7 @@ public async Task Can_Create_Usergroup_With_Empty_Granluar_Permissions_For_Docum
124124
}
125125
};
126126

127-
var attempt = await UserGroupPresentationFactory.CreateAsync(updateModel);
127+
var attempt = await UserGroupPresentationFactory.CreateAsync(createModel);
128128
Assert.IsTrue(attempt.Success);
129129

130130
var userGroupCreateAttempt = await UserGroupService.CreateAsync(attempt.Result, Constants.Security.SuperUserKey);

0 commit comments

Comments
 (0)