[v14] Add missing alias and id to usergroup related api models (#16154)

* Added missing alias and Id to usergroup models

create/update/response/item

* Changed userGroup IsSystemGroup to more meaningfull fields

Also enforced the AliasCanBeChanged businessrule 🙈

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
Co-authored-by: Mads Rasmussen <madsr@hey.com>
This commit is contained in:
Sven Geusens
2024-05-03 10:24:09 +02:00
committed by GitHub
parent 8ad6c36038
commit f9c0235a35
13 changed files with 175 additions and 28 deletions

View File

@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Security.Authorization.UserGroup;
using Umbraco.Cms.Api.Management.ViewModels.UserGroup;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security.Authorization;

View File

@@ -29,11 +29,15 @@ public class UserGroupControllerBase : ManagementApiControllerBase
.WithTitle("Duplicate alias")
.WithDetail("A user group already exists with the attempted alias.")
.Build()),
UserGroupOperationStatus.CanNotUpdateAliasIsSystemUserGroup => BadRequest(problemDetailsBuilder
.WithTitle("System user group")
.WithDetail("Changing the alias is not allowed on a system user group.")
.Build()),
UserGroupOperationStatus.MissingUser => Unauthorized(problemDetailsBuilder
.WithTitle("Missing user")
.WithDetail("A performing user was not found when attempting the operation.")
.Build()),
UserGroupOperationStatus.IsSystemUserGroup => BadRequest(problemDetailsBuilder
UserGroupOperationStatus.CanNotDeleteIsSystemUserGroup => BadRequest(problemDetailsBuilder
.WithTitle("System user group")
.WithDetail("The operation is not allowed on a system user group.")
.Build()),

View File

@@ -51,8 +51,9 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
return new UserGroupResponseModel
{
Name = userGroup.Name ?? string.Empty,
Id = userGroup.Key,
Name = userGroup.Name ?? string.Empty,
Alias = userGroup.Alias,
DocumentStartNode = ReferenceByIdModel.ReferenceOrNull(contentStartNodeKey),
DocumentRootAccess = contentRootAccess,
MediaStartNode = ReferenceByIdModel.ReferenceOrNull(mediaStartNodeKey),
@@ -63,7 +64,8 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
FallbackPermissions = userGroup.Permissions,
Permissions = await _permissionPresentationFactory.CreateAsync(userGroup.GranularPermissions),
Sections = userGroup.AllowedSections.Select(SectionMapper.GetName),
IsSystemGroup = userGroup.IsSystemUserGroup()
IsDeletable = !userGroup.IsSystemUserGroup(),
AliasCanBeChanged = !userGroup.IsSystemUserGroup(),
};
}
@@ -83,8 +85,9 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
return new UserGroupResponseModel
{
Name = userGroup.Name ?? string.Empty,
Id = userGroup.Key,
Name = userGroup.Name ?? string.Empty,
Alias = userGroup.Alias,
DocumentStartNode = ReferenceByIdModel.ReferenceOrNull(contentStartNodeKey),
MediaStartNode = ReferenceByIdModel.ReferenceOrNull(mediaStartNodeKey),
Icon = userGroup.Icon,
@@ -93,6 +96,8 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
FallbackPermissions = userGroup.Permissions,
Permissions = await _permissionPresentationFactory.CreateAsync(userGroup.GranularPermissions),
Sections = userGroup.AllowedSections.Select(SectionMapper.GetName),
IsDeletable = !userGroup.IsSystemUserGroup(),
AliasCanBeChanged = !userGroup.IsSystemUserGroup(),
};
}
@@ -107,6 +112,7 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
return userGroupViewModels;
}
/// <inheritdoc />
public async Task<IEnumerable<UserGroupResponseModel>> CreateMultipleAsync(IEnumerable<IReadOnlyUserGroup> userGroups)
{
@@ -122,18 +128,21 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
/// <inheritdoc />
public async Task<Attempt<IUserGroup, UserGroupOperationStatus>> CreateAsync(CreateUserGroupRequestModel requestModel)
{
var cleanedName = requestModel.Name.CleanForXss('[', ']', '(', ')', ':');
var group = new UserGroup(_shortStringHelper)
{
Name = cleanedName,
Alias = cleanedName,
Name = CleanUserGroupNameOrAliasForXss(requestModel.Name),
Alias = CleanUserGroupNameOrAliasForXss(requestModel.Alias),
Icon = requestModel.Icon,
HasAccessToAllLanguages = requestModel.HasAccessToAllLanguages,
Permissions = requestModel.FallbackPermissions,
GranularPermissions = await _permissionPresentationFactory.CreatePermissionSetsAsync(requestModel.Permissions)
GranularPermissions = await _permissionPresentationFactory.CreatePermissionSetsAsync(requestModel.Permissions),
};
if (requestModel.Id.HasValue)
{
group.Key = requestModel.Id.Value;
}
Attempt<UserGroupOperationStatus> assignmentAttempt = AssignStartNodesToUserGroup(requestModel, group);
if (assignmentAttempt.Success is false)
{
@@ -186,7 +195,8 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
current.AddAllowedSection(SectionMapper.GetAlias(sectionName));
}
current.Name = request.Name.CleanForXss('[', ']', '(', ')', ':');
current.Name = CleanUserGroupNameOrAliasForXss(request.Name);
current.Alias = CleanUserGroupNameOrAliasForXss(request.Alias);
current.Icon = request.Icon;
current.HasAccessToAllLanguages = request.HasAccessToAllLanguages;
@@ -196,6 +206,9 @@ public class UserGroupPresentationFactory : IUserGroupPresentationFactory
return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, current);
}
private static string CleanUserGroupNameOrAliasForXss(string input)
=> input.CleanForXss('[', ']', '(', ')', ':');
private async Task<Attempt<IEnumerable<string>, UserGroupOperationStatus>> MapLanguageIdsToIsoCodeAsync(IEnumerable<int> ids)
{
IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();

View File

@@ -112,6 +112,7 @@ public class ItemTypeMapDefinition : IMapDefinition
target.Id = source.Key;
target.Name = source.Name ?? source.Alias;
target.Icon = source.Icon;
target.Alias = source.Alias;
}
// Umbraco.Code.MapAll

View File

@@ -34131,6 +34131,7 @@
},
"CreateUserGroupRequestModel": {
"required": [
"alias",
"documentRootAccess",
"fallbackPermissions",
"hasAccessToAllLanguages",
@@ -34145,6 +34146,9 @@
"name": {
"type": "string"
},
"alias": {
"type": "string"
},
"icon": {
"type": "string",
"nullable": true
@@ -34206,6 +34210,11 @@
}
]
}
},
"id": {
"type": "string",
"format": "uuid",
"nullable": true
}
},
"additionalProperties": false
@@ -43105,6 +43114,7 @@
},
"UpdateUserGroupRequestModel": {
"required": [
"alias",
"documentRootAccess",
"fallbackPermissions",
"hasAccessToAllLanguages",
@@ -43119,6 +43129,9 @@
"name": {
"type": "string"
},
"alias": {
"type": "string"
},
"icon": {
"type": "string",
"nullable": true
@@ -43432,12 +43445,17 @@
"icon": {
"type": "string",
"nullable": true
},
"alias": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"UserGroupResponseModel": {
"required": [
"alias",
"documentRootAccess",
"fallbackPermissions",
"hasAccessToAllLanguages",
@@ -43454,6 +43472,9 @@
"name": {
"type": "string"
},
"alias": {
"type": "string"
},
"icon": {
"type": "string",
"nullable": true

View File

@@ -2,5 +2,5 @@
public class CreateUserGroupRequestModel : UserGroupBase
{
public Guid? Id { get; set; }
}

View File

@@ -5,4 +5,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup.Item;
public class UserGroupItemResponseModel : NamedItemResponseModelBase
{
public string? Icon { get; set; }
public string? Alias { get; set; }
}

View File

@@ -17,6 +17,11 @@ public class UserGroupBase
/// </summary>
public required string Name { get; init; }
/// <summary>
/// The alias of the user groups
/// </summary>
public required string Alias { get; init; }
/// <summary>
/// The Icon for the user group
/// </summary>

View File

@@ -10,5 +10,10 @@ public class UserGroupResponseModel : UserGroupBase
/// <summary>
/// Whether this user group is required at system level (thus cannot be removed)
/// </summary>
public bool IsSystemGroup { get; set; }
public bool IsDeletable { get; set; }
/// <summary>
/// Whether this user group is required at system level (thus alias needs to be fixed)
/// </summary>
public bool AliasCanBeChanged { get; set; }
}

View File

@@ -8,7 +8,8 @@ public enum UserGroupOperationStatus
AlreadyExists,
DuplicateAlias,
MissingUser,
IsSystemUserGroup,
CanNotDeleteIsSystemUserGroup,
CanNotUpdateAliasIsSystemUserGroup,
CancelledByNotification,
MediaStartNodeKeyNotFound,
DocumentStartNodeKeyNotFound,

View File

@@ -263,7 +263,7 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
if (userGroup.IsSystemUserGroup())
{
return Attempt.Fail(UserGroupOperationStatus.IsSystemUserGroup);
return Attempt.Fail(UserGroupOperationStatus.CanNotDeleteIsSystemUserGroup);
}
return Attempt.Succeed(UserGroupOperationStatus.Success);
@@ -520,12 +520,18 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
return UserGroupOperationStatus.NotFound;
}
IUserGroup? existing = _userGroupRepository.Get(userGroup.Alias);
if (existing is not null && existing.Key != userGroup.Key)
IUserGroup? existingByAlias = _userGroupRepository.Get(userGroup.Alias);
if (existingByAlias is not null && existingByAlias.Key != userGroup.Key)
{
return UserGroupOperationStatus.DuplicateAlias;
}
IUserGroup? existingByKey = await GetAsync(userGroup.Key);
if (existingByKey is not null && existingByKey.IsSystemUserGroup() && existingByKey.Alias != userGroup.Alias)
{
return UserGroupOperationStatus.CanNotUpdateAliasIsSystemUserGroup;
}
return UserGroupOperationStatus.Success;
}

View File

@@ -185,7 +185,7 @@ public class UserGroupServiceValidationTests : UmbracoIntegrationTest
var result = await UserGroupService.DeleteAsync(key);
Assert.IsFalse(result.Success);
Assert.AreEqual(UserGroupOperationStatus.IsSystemUserGroup, result.Result);
Assert.AreEqual(UserGroupOperationStatus.CanNotDeleteIsSystemUserGroup, result.Result);
}
// these keys are not defined as "const" in Constants.Security but as "static readonly", so we have to hardcode

View File

@@ -1,12 +1,17 @@
using System.Data;
using System.Linq.Expressions;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
@@ -21,7 +26,7 @@ public class UserGroupServiceTests
public async Task Filter_Returns_Only_User_Groups_For_Non_Admin(params string[] userGroupAliases)
{
var userKey = Guid.NewGuid();
var userGroupService = SetupUserGroupService(userKey, userGroupAliases);
var userGroupService = SetupUserGroupServiceWithUserAndGetManyReturnsFourGroups(userKey, userGroupAliases);
var result = await userGroupService.FilterAsync(userKey, null, 0, 10);
Assert.Multiple(() =>
@@ -41,7 +46,7 @@ public class UserGroupServiceTests
public async Task Filter_Does_Not_Return_Non_Existing_Groups(params string[] userGroupAliases)
{
var userKey = Guid.NewGuid();
var userGroupService = SetupUserGroupService(userKey, userGroupAliases);
var userGroupService = SetupUserGroupServiceWithUserAndGetManyReturnsFourGroups(userKey, userGroupAliases);
var result = await userGroupService.FilterAsync(userKey, null, 0, 10);
Assert.Multiple(() =>
@@ -55,7 +60,7 @@ public class UserGroupServiceTests
public async Task Filter_Returns_All_Groups_For_Admin()
{
var userKey = Guid.NewGuid();
var userGroupService = SetupUserGroupService(userKey, new [] { Constants.Security.AdminGroupAlias });
var userGroupService = SetupUserGroupServiceWithUserAndGetManyReturnsFourGroups(userKey, new [] { Constants.Security.AdminGroupAlias });
var result = await userGroupService.FilterAsync(userKey, null, 0, 10);
Assert.Multiple(() =>
@@ -73,7 +78,7 @@ public class UserGroupServiceTests
public async Task Filter_Can_Filter_By_Group_Name()
{
var userKey = Guid.NewGuid();
var userGroupService = SetupUserGroupService(userKey, new [] { Constants.Security.AdminGroupAlias });
var userGroupService = SetupUserGroupServiceWithUserAndGetManyReturnsFourGroups(userKey, new [] { Constants.Security.AdminGroupAlias });
var result = await userGroupService.FilterAsync(userKey, "e", 0, 10);
Assert.Multiple(() =>
@@ -85,6 +90,79 @@ public class UserGroupServiceTests
});
}
[TestCase(false,UserGroupOperationStatus.Success)]
[TestCase(true,UserGroupOperationStatus.CanNotUpdateAliasIsSystemUserGroup)]
public async Task Can_Not_Update_SystemGroup_Alias(bool isSystemGroup, UserGroupOperationStatus status)
{
// Arrange
var actingUserKey = Guid.NewGuid();
var mockUser = SetupUserWithGroupAccess(actingUserKey, [Constants.Security.AdminGroupAlias]);
var userService = SetupUserServiceWithGetUserByKey(actingUserKey, mockUser);
var userGroupRepository = new Mock<IUserGroupRepository>();
var userGroupKey = Guid.NewGuid();
var persistedUserGroup =
new UserGroup(
Mock.Of<IShortStringHelper>(),
0,
isSystemGroup ? Constants.Security.AdminGroupAlias : "someNonSystemAlias",
"Administrators",
null)
{
Id = 10,
Key = userGroupKey,
};
userGroupRepository
.Setup(r => r.Get(It.IsAny<IQuery<IUserGroup>>()))
.Returns(new[]
{
persistedUserGroup
});
var updatingUserGroup = new UserGroup(Mock.Of<IShortStringHelper>(), 0, persistedUserGroup.Alias + "updated",
persistedUserGroup.Name + "updated", null)
{
Key = persistedUserGroup.Key,
Id = persistedUserGroup.Id
};
var scopedNotificationPublisher = new Mock<IScopedNotificationPublisher>();
scopedNotificationPublisher.Setup(p => p.PublishCancelableAsync(It.IsAny<ICancelableNotification>()))
.ReturnsAsync(false);
var scope = new Mock<ICoreScope>();
scope.SetupGet(s => s.Notifications).Returns(scopedNotificationPublisher.Object);
var query = new Mock<IQuery<IUserGroup>>();
query.Setup(q => q.Where(It.IsAny<Expression<Func<IUserGroup, bool>>>())).Returns(query.Object);
var provider = new Mock<ICoreScopeProvider>();
provider.Setup(p => p.CreateQuery<IUserGroup>()).Returns(query.Object);
provider.Setup(p => p.CreateCoreScope(
It.IsAny<IsolationLevel>(),
It.IsAny<RepositoryCacheMode>(),
It.IsAny<IEventDispatcher?>(),
It.IsAny<IScopedNotificationPublisher?>(),
It.IsAny<bool?>(),
It.IsAny<bool>(),
It.IsAny<bool>()))
.Returns(scope.Object);
var service = new UserGroupService(
provider.Object,
Mock.Of<ILoggerFactory>(),
Mock.Of<IEventMessagesFactory>(),
userGroupRepository.Object,
Mock.Of<IUserGroupPermissionService>(),
Mock.Of<IEntityService>(),
userService.Object,
Mock.Of<ILogger<UserGroupService>>());
// act
var updateAttempt = await service.UpdateAsync(updatingUserGroup, actingUserKey);
// assert
Assert.AreEqual(status, updateAttempt.Status);
}
private IEnumerable<IReadOnlyUserGroup> CreateGroups(params string[] aliases)
=> aliases.Select(alias =>
{
@@ -93,14 +171,11 @@ public class UserGroupServiceTests
return group.Object;
}).ToArray();
private IUserGroupService SetupUserGroupService(Guid userKey, string[] userGroupAliases)
private IUserGroupService SetupUserGroupServiceWithUserAndGetManyReturnsFourGroups(Guid userKey, string[] userGroupAliases)
{
var user = new Mock<IUser>();
user.SetupGet(u => u.Key).Returns(userKey);
user.Setup(u => u.Groups).Returns(CreateGroups(userGroupAliases));
var mockUser = SetupUserWithGroupAccess(userKey, userGroupAliases);
var userService = new Mock<IUserService>();
userService.Setup(s => s.GetAsync(userKey)).Returns(Task.FromResult(user.Object));
var userService = SetupUserServiceWithGetUserByKey(userKey, mockUser);
var userGroupRepository = new Mock<IUserGroupRepository>();
userGroupRepository
@@ -123,4 +198,19 @@ public class UserGroupServiceTests
userService.Object,
Mock.Of<ILogger<UserGroupService>>());
}
private Mock<IUser> SetupUserWithGroupAccess(Guid userKey, string[] userGroupAliases)
{
var user = new Mock<IUser>();
user.SetupGet(u => u.Key).Returns(userKey);
user.Setup(u => u.Groups).Returns(CreateGroups(userGroupAliases));
return user;
}
private Mock<IUserService> SetupUserServiceWithGetUserByKey(Guid userKey, Mock<IUser> mockUser)
{
var userService = new Mock<IUserService>();
userService.Setup(s => s.GetAsync(userKey)).Returns(Task.FromResult(mockUser.Object));
return userService;
}
}