diff --git a/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs index 43db8355b7..a6faf7ddbe 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/User/UserOrCurrentUserControllerBase.cs @@ -25,7 +25,7 @@ public abstract class UserOrCurrentUserControllerBase : ManagementApiControllerB .Build()), UserOperationStatus.NoUserGroup => BadRequest(problemDetailsBuilder .WithTitle("No User Group Specified") - .WithDetail("A user group must be specified to create a user") + .WithDetail("A user must be assigned to at least one group") .Build()), UserOperationStatus.UserNameIsNotEmail => BadRequest(problemDetailsBuilder .WithTitle("Invalid Username") diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 718ef1eb69..612e32efde 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -883,7 +883,7 @@ internal partial class UserService : RepositoryService, IUserService return UserOperationStatus.DuplicateUserName; } - if(model.UserGroupKeys.Count == 0) + if (model.UserGroupKeys.Count == 0) { return UserOperationStatus.NoUserGroup; } @@ -912,6 +912,13 @@ internal partial class UserService : RepositoryService, IUserService return Attempt.FailWithStatus(UserOperationStatus.MissingUser, existingUser); } + // A user must remain assigned to at least one group. + if (model.UserGroupKeys.Count == 0) + { + scope.Complete(); + return Attempt.FailWithStatus(UserOperationStatus.NoUserGroup, existingUser); + } + // User names can only contain the configured allowed characters. This is validated by ASP.NET Identity on create // as the setting is applied to the BackOfficeIdentityOptions, but we need to check ourselves for updates. var allowedUserNameCharacters = _securitySettings.AllowedUserNameCharacters; diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts index 3b8b6b34ec..0ede3be26b 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/User.spec.ts @@ -642,4 +642,4 @@ test.skip('cannot remove all user group from a user', {tag: '@release'}, async ( // Assert await umbracoUi.user.isErrorNotificationVisible(); -}); \ No newline at end of file +}); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Update.cs index 4b5cc29fee..f2d8ab68b8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Update.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Update.cs @@ -151,7 +151,21 @@ internal sealed partial class UserServiceCrudTests Assert.AreEqual(email, updatedUser.Email); Assert.AreEqual(name, updatedUser.Name); }); + } + [Test] + public async Task Cannot_Update_User_To_Have_No_Groups() + { + var userService = CreateUserService(securitySettings: new SecuritySettings { UsernameIsEmail = false }); + + var (updateModel, createdUser) = await CreateUserForUpdate(userService); + + updateModel.UserGroupKeys.Clear(); + + var updateAttempt = await userService.UpdateAsync(Constants.Security.SuperUserKey, updateModel); + + Assert.IsFalse(updateAttempt.Success); + Assert.AreEqual(UserOperationStatus.NoUserGroup, updateAttempt.Status); } [Test]