Add post and delete methods to user-group/id/users to add/remove user… (#15490)
* Add post and delete methods to user-group/id/users to add/remove users from group * Update OpenApi contract * PR feedbac Moved fetch usergroup logic into service layer Renamed methods to async * Naming * Introduced user not found on UserGroupOperationStatus, as otherwise the return message is wrong * Added authentication * Removed authorization from the service layer as its determined that that responsibiliity does not belong there. --------- Co-authored-by: Sven Geusens <sge@umbraco.dk> Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
using Asp.Versioning;
|
||||
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;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.UserGroup;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class AddUsersToUserGroupController : UserGroupControllerBase
|
||||
{
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public AddUsersToUserGroupController(
|
||||
IUserGroupService userGroupService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_userGroupService = userGroupService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/users")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Update(Guid id, Guid[] userIds)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
UserGroupPermissionResource.WithKeys(id),
|
||||
AuthorizationPolicies.UserBelongsToUserGroupInRequest);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
UserGroupOperationStatus result = await _userGroupService.AddUsersToUserGroupAsync(
|
||||
new UsersToUserGroupManipulationModel(id, userIds), CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result == UserGroupOperationStatus.Success
|
||||
? Ok()
|
||||
: UserGroupOperationStatusResult(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Asp.Versioning;
|
||||
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;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.UserGroup;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class RemoveUsersFromUserGroupController : UserGroupControllerBase
|
||||
{
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public RemoveUsersFromUserGroupController(
|
||||
IUserGroupService userGroupService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_userGroupService = userGroupService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}/users")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Update(Guid id, Guid[] userIds)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
UserGroupPermissionResource.WithKeys(id),
|
||||
AuthorizationPolicies.UserBelongsToUserGroupInRequest);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
UserGroupOperationStatus result = await _userGroupService.RemoveUsersFromUserGroupAsync(
|
||||
new UsersToUserGroupManipulationModel(id, userIds), CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result == UserGroupOperationStatus.Success
|
||||
? Ok()
|
||||
: UserGroupOperationStatusResult(result);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,10 @@ public class UserGroupControllerBase : ManagementApiControllerBase
|
||||
status switch
|
||||
{
|
||||
UserGroupOperationStatus.NotFound => UserGroupNotFound(),
|
||||
UserGroupOperationStatus.UserNotFound => NotFound(new ProblemDetailsBuilder()
|
||||
.WithTitle("User key not found")
|
||||
.WithDetail("The provided user key do not exist.")
|
||||
.Build()),
|
||||
UserGroupOperationStatus.AlreadyExists => Conflict(new ProblemDetailsBuilder()
|
||||
.WithTitle("User group already exists")
|
||||
.WithDetail("The user group exists already.")
|
||||
|
||||
@@ -16930,6 +16930,170 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/user-group/{id}/users": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"User Group"
|
||||
],
|
||||
"operationId": "DeleteUserGroupByIdUsers",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "The resource is protected and requires an authentication token"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"Backoffice User": [ ]
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"User Group"
|
||||
],
|
||||
"operationId": "PostUserGroupByIdUsers",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success"
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "The resource is protected and requires an authentication token"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"Backoffice User": [ ]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/user-group/item": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -25541,4 +25705,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Umbraco.Core/Models/UsersToUserGroupManipulationModel.cs
Normal file
15
src/Umbraco.Core/Models/UsersToUserGroupManipulationModel.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
public class UsersToUserGroupManipulationModel
|
||||
{
|
||||
public Guid UserGroupKey { get; init; }
|
||||
public Guid[] UserKeys { get; init; }
|
||||
|
||||
public UsersToUserGroupManipulationModel(Guid userGroupKey, Guid[] userKeys)
|
||||
{
|
||||
UserGroupKey = userGroupKey;
|
||||
UserKeys = userKeys;
|
||||
}
|
||||
}
|
||||
@@ -95,4 +95,7 @@ public interface IUserGroupService
|
||||
/// <param name="userKeys">The user whose groups we want to alter.</param>
|
||||
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
|
||||
Task UpdateUserGroupsOnUsers(ISet<Guid> userGroupKeys, ISet<Guid> userKeys);
|
||||
|
||||
Task<UserGroupOperationStatus> AddUsersToUserGroupAsync(UsersToUserGroupManipulationModel addUsersModel, Guid performingUserKey);
|
||||
Task<UserGroupOperationStatus> RemoveUsersFromUserGroupAsync(UsersToUserGroupManipulationModel removeUsersModel, Guid performingUserKey);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ public enum UserGroupOperationStatus
|
||||
{
|
||||
Success,
|
||||
NotFound,
|
||||
UserNotFound,
|
||||
AlreadyExists,
|
||||
DuplicateAlias,
|
||||
MissingUser,
|
||||
|
||||
@@ -49,11 +49,7 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
|
||||
var total = groups.Length;
|
||||
|
||||
return Task.FromResult(new PagedModel<IUserGroup>
|
||||
{
|
||||
Items = groups.Skip(skip).Take(take),
|
||||
Total = total,
|
||||
});
|
||||
return Task.FromResult(new PagedModel<IUserGroup> { Items = groups.Skip(skip).Take(take), Total = total, });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -173,7 +169,8 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
_userGroupRepository.Delete(userGroup);
|
||||
}
|
||||
|
||||
scope.Notifications.Publish(new UserGroupDeletedNotification(userGroupsToDelete, eventMessages).WithStateFrom(deletingNotification));
|
||||
scope.Notifications.Publish(
|
||||
new UserGroupDeletedNotification(userGroupsToDelete, eventMessages).WithStateFrom(deletingNotification));
|
||||
|
||||
scope.Complete();
|
||||
|
||||
@@ -190,7 +187,7 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
.Select(x => x.ToReadOnlyGroup())
|
||||
.ToArray();
|
||||
|
||||
foreach(IUser user in users)
|
||||
foreach (IUser user in users)
|
||||
{
|
||||
user.ClearGroups();
|
||||
foreach (IReadOnlyUserGroup userGroup in userGroups)
|
||||
@@ -231,13 +228,15 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
return Attempt.FailWithStatus(UserGroupOperationStatus.MissingUser, userGroup);
|
||||
}
|
||||
|
||||
Attempt<IUserGroup, UserGroupOperationStatus> validationAttempt = await ValidateUserGroupCreationAsync(userGroup);
|
||||
Attempt<IUserGroup, UserGroupOperationStatus> validationAttempt =
|
||||
await ValidateUserGroupCreationAsync(userGroup);
|
||||
if (validationAttempt.Success is false)
|
||||
{
|
||||
return validationAttempt;
|
||||
}
|
||||
|
||||
UserGroupAuthorizationStatus isAuthorized = await _userGroupPermissionService.AuthorizeCreateAsync(performingUser, userGroup);
|
||||
UserGroupAuthorizationStatus isAuthorized =
|
||||
await _userGroupPermissionService.AuthorizeCreateAsync(performingUser, userGroup);
|
||||
if (isAuthorized != UserGroupAuthorizationStatus.Success)
|
||||
{
|
||||
// Convert from UserGroupAuthorizationStatus to UserGroupOperationStatus
|
||||
@@ -253,13 +252,16 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
return Attempt.FailWithStatus(UserGroupOperationStatus.CancelledByNotification, userGroup);
|
||||
}
|
||||
|
||||
Guid[] checkedGroupMembersKeys = EnsureNonAdminUserIsInSavedUserGroup(performingUser, groupMembersKeys ?? Enumerable.Empty<Guid>()).ToArray();
|
||||
Guid[] checkedGroupMembersKeys =
|
||||
EnsureNonAdminUserIsInSavedUserGroup(performingUser, groupMembersKeys ?? Enumerable.Empty<Guid>())
|
||||
.ToArray();
|
||||
IUser[] usersToAdd = (await _userService.GetAsync(checkedGroupMembersKeys)).ToArray();
|
||||
|
||||
// Since this is a brand new creation we don't have to be worried about what users were added and removed
|
||||
// simply put all members that are requested to be in the group will be "added"
|
||||
var userGroupWithUsers = new UserGroupWithUsers(userGroup, usersToAdd, Array.Empty<IUser>());
|
||||
var savingUserGroupWithUsersNotification = new UserGroupWithUsersSavingNotification(userGroupWithUsers, eventMessages);
|
||||
var savingUserGroupWithUsersNotification =
|
||||
new UserGroupWithUsersSavingNotification(userGroupWithUsers, eventMessages);
|
||||
if (await scope.Notifications.PublishCancelableAsync(savingUserGroupWithUsersNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
@@ -272,7 +274,8 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, userGroup);
|
||||
}
|
||||
|
||||
private async Task<Attempt<IUserGroup, UserGroupOperationStatus>> ValidateUserGroupCreationAsync(IUserGroup userGroup)
|
||||
private async Task<Attempt<IUserGroup, UserGroupOperationStatus>> ValidateUserGroupCreationAsync(
|
||||
IUserGroup userGroup)
|
||||
{
|
||||
if (await IsNewUserGroup(userGroup) is false)
|
||||
{
|
||||
@@ -312,7 +315,8 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
return Attempt.FailWithStatus(validationStatus, userGroup);
|
||||
}
|
||||
|
||||
UserGroupAuthorizationStatus isAuthorized = await _userGroupPermissionService.AuthorizeUpdateAsync(performingUser, userGroup);
|
||||
UserGroupAuthorizationStatus isAuthorized =
|
||||
await _userGroupPermissionService.AuthorizeUpdateAsync(performingUser, userGroup);
|
||||
if (isAuthorized != UserGroupAuthorizationStatus.Success)
|
||||
{
|
||||
// Convert from UserGroupAuthorizationStatus to UserGroupOperationStatus
|
||||
@@ -329,12 +333,81 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
}
|
||||
|
||||
_userGroupRepository.Save(userGroup);
|
||||
scope.Notifications.Publish(new UserGroupSavedNotification(userGroup, eventMessages).WithStateFrom(savingNotification));
|
||||
scope.Notifications.Publish(
|
||||
new UserGroupSavedNotification(userGroup, eventMessages).WithStateFrom(savingNotification));
|
||||
|
||||
scope.Complete();
|
||||
return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, userGroup);
|
||||
}
|
||||
|
||||
public async Task<UserGroupOperationStatus> AddUsersToUserGroupAsync(UsersToUserGroupManipulationModel addUsersModel, Guid performingUserKey)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
|
||||
UserGroupOperationStatus result = await SafelyManipulateUsersBasedOnGroupAsync(addUsersModel, performingUserKey, (users, group) =>
|
||||
{
|
||||
IReadOnlyUserGroup readOnlyGroup = group.ToReadOnlyGroup();
|
||||
|
||||
foreach (IUser user in users)
|
||||
{
|
||||
user.AddGroup(readOnlyGroup);
|
||||
}
|
||||
});
|
||||
|
||||
scope.Complete();
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<UserGroupOperationStatus> RemoveUsersFromUserGroupAsync(UsersToUserGroupManipulationModel removeUsersModel, Guid performingUserKey)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
|
||||
UserGroupOperationStatus result = await SafelyManipulateUsersBasedOnGroupAsync(removeUsersModel, performingUserKey, (users, group) =>
|
||||
{
|
||||
foreach (IUser user in users)
|
||||
{
|
||||
user.RemoveGroup(group.Alias);
|
||||
}
|
||||
});
|
||||
|
||||
scope.Complete();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether all users that are part of the manipulation exist,
|
||||
/// performs the manipulation,
|
||||
/// saves the users
|
||||
/// </summary>
|
||||
private async Task<UserGroupOperationStatus> SafelyManipulateUsersBasedOnGroupAsync(UsersToUserGroupManipulationModel assignModel, Guid performingUserKey, Action<IUser[], IUserGroup> manipulation)
|
||||
{
|
||||
IUser? performingUser = await _userService.GetAsync(performingUserKey);
|
||||
if (performingUser is null)
|
||||
{
|
||||
return UserGroupOperationStatus.MissingUser;
|
||||
}
|
||||
|
||||
IUserGroup? existingUserGroup = await GetAsync(assignModel.UserGroupKey);
|
||||
|
||||
if (existingUserGroup is null)
|
||||
{
|
||||
return UserGroupOperationStatus.NotFound;
|
||||
}
|
||||
|
||||
IUser[] users = (await _userService.GetAsync(assignModel.UserKeys)).ToArray();
|
||||
|
||||
if (users.Length != assignModel.UserKeys.Length)
|
||||
{
|
||||
return UserGroupOperationStatus.UserNotFound;
|
||||
}
|
||||
|
||||
manipulation(users, existingUserGroup);
|
||||
|
||||
_userService.Save(users);
|
||||
|
||||
return UserGroupOperationStatus.Success;
|
||||
}
|
||||
|
||||
private async Task<UserGroupOperationStatus> ValidateUserGroupUpdateAsync(IUserGroup userGroup)
|
||||
{
|
||||
UserGroupOperationStatus commonValidationStatus = ValidateCommon(userGroup);
|
||||
@@ -421,7 +494,8 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
/// <remarks>
|
||||
/// This is to ensure that the user can access the group they themselves created at a later point and modify it.
|
||||
/// </remarks>
|
||||
private IEnumerable<Guid> EnsureNonAdminUserIsInSavedUserGroup(IUser performingUser, IEnumerable<Guid> groupMembersUserKeys)
|
||||
private IEnumerable<Guid> EnsureNonAdminUserIsInSavedUserGroup(IUser performingUser,
|
||||
IEnumerable<Guid> groupMembersUserKeys)
|
||||
{
|
||||
var userKeys = groupMembersUserKeys.ToList();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user