!fix(users): restrict DeleteUser to Admin [SH-221]

Align DeleteUser with AddUser/EditUser: Admin role at controller and
service entry, Forbidden for non-Admin, and regression coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Arthur Bassi 2026-08-11 09:44:52 -03:00
parent d0724a0ac5
commit 446e43b2c6
5 changed files with 71 additions and 17 deletions

View file

@ -183,10 +183,13 @@ public class UserControllerTests
public async Task DeleteUser_NotFound_ReturnsIdNotMatchedMessage()
{
var service = new Mock<IUserService>();
service.Setup(s => s.DeleteUserAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(false);
service.Setup(s => s.DeleteUserAsync(
It.IsAny<string>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = false, Error = UserMutationErrors.UserNotFound });
var controller = NewController(service);
var controller = NewController(service, "admin-1", "Admin");
var result = await controller.DeleteUser(new EditUser_DTO { Id = "missing" }, CancellationToken.None);
@ -199,14 +202,38 @@ public class UserControllerTests
public async Task DeleteUser_Found_DelegatesAndReturnsOk()
{
var service = new Mock<IUserService>();
service.Setup(s => s.DeleteUserAsync("5", It.IsAny<CancellationToken>())).ReturnsAsync(true);
service.Setup(s => s.DeleteUserAsync(
"5",
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = true });
var controller = NewController(service);
var controller = NewController(service, "admin-1", "Admin");
var result = await controller.DeleteUser(new EditUser_DTO { Id = "5" }, CancellationToken.None);
result.Should().BeOfType<OkObjectResult>();
service.Verify(s => s.DeleteUserAsync("5", It.IsAny<CancellationToken>()), Times.Once);
service.Verify(s => s.DeleteUserAsync(
"5",
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task DeleteUser_Forbidden_ReturnsForbid()
{
var service = new Mock<IUserService>();
service.Setup(s => s.DeleteUserAsync(
It.IsAny<string>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = false, Error = UserMutationErrors.Forbidden });
var controller = NewController(service, "tech-1", "User");
var result = await controller.DeleteUser(new EditUser_DTO { Id = "5" }, CancellationToken.None);
result.Should().BeOfType<ForbidResult>();
}
[Fact]

View file

@ -61,16 +61,17 @@ public class UserServiceTests
}
[Fact]
public async Task DeleteUser_NotFound_ReturnsFalseWithoutCascade()
public async Task DeleteUser_NotFound_ReturnsUserNotFoundWithoutCascade()
{
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetForEditAsync("missing", It.IsAny<CancellationToken>())).ReturnsAsync((ApplicationUser?)null);
var service = NewService(userData, new Mock<IEmailSender>(), out _);
var found = await service.DeleteUserAsync("missing", CancellationToken.None);
var outcome = await service.DeleteUserAsync("missing", Principal("Admin"), CancellationToken.None);
found.Should().BeFalse();
outcome.Success.Should().BeFalse();
outcome.Error.Should().Be(UserMutationErrors.UserNotFound);
userData.Verify(u => u.DeleteUserWithCascadeAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
}
@ -83,12 +84,28 @@ public class UserServiceTests
var service = NewService(userData, new Mock<IEmailSender>(), out _);
var found = await service.DeleteUserAsync("u1", CancellationToken.None);
var outcome = await service.DeleteUserAsync("u1", Principal("Admin"), CancellationToken.None);
found.Should().BeTrue();
outcome.Success.Should().BeTrue();
userData.Verify(u => u.DeleteUserWithCascadeAsync(user, It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task DeleteUser_NonAdmin_ReturnsForbiddenWithoutCascade()
{
var user = IdentityTestHelpers.User();
var userData = new Mock<IUserDataService>();
userData.Setup(u => u.GetForEditAsync("u1", It.IsAny<CancellationToken>())).ReturnsAsync(user);
var service = NewService(userData, new Mock<IEmailSender>(), out _);
var outcome = await service.DeleteUserAsync("u1", Principal("User"), CancellationToken.None);
outcome.Success.Should().BeFalse();
outcome.Error.Should().Be(UserMutationErrors.Forbidden);
userData.Verify(u => u.DeleteUserWithCascadeAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task DeleteCurrentUser_SetsIsDeletedFlag()
{

View file

@ -94,14 +94,17 @@ namespace Api.SeaHavenIndustries.Controllers
[HttpDelete]
[Route("DeleteUser")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> DeleteUser(EditUser_DTO req, CancellationToken cancellationToken)
{
string id = req.Id ?? "";
try
{
var found = await _userService.DeleteUserAsync(id, cancellationToken);
if (!found)
var outcome = await _userService.DeleteUserAsync(id, User, cancellationToken);
if (!outcome.Success)
{
if (string.Equals(outcome.Error, UserMutationErrors.Forbidden, StringComparison.Ordinal))
return Forbid();
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
}
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });

View file

@ -161,14 +161,21 @@ namespace SeaHaven.Services.Implementation
return new AddUserOutcomeDTO { Success = true };
}
public async Task<bool> DeleteUserAsync(string id, CancellationToken cancellationToken)
public async Task<AddUserOutcomeDTO> DeleteUserAsync(
string id,
ClaimsPrincipal caller,
CancellationToken cancellationToken)
{
var authFailure = EnsureAdminCaller(caller);
if (authFailure != null)
return authFailure;
var data = await _userDataService.GetForEditAsync(id, cancellationToken);
if (data == null)
return false;
return new AddUserOutcomeDTO { Success = false, Error = UserMutationErrors.UserNotFound };
await _userDataService.DeleteUserWithCascadeAsync(data, cancellationToken);
return true;
return new AddUserOutcomeDTO { Success = true };
}
public async Task DeleteCurrentUserAsync(string userId, CancellationToken cancellationToken)

View file

@ -8,7 +8,7 @@ namespace SeaHaven.Services.Interfaces
Task<IEnumerable<UserListRowDTO>> GetUsersAsync(CancellationToken cancellationToken);
Task<AddUserOutcomeDTO> AddUserAsync(AddUserRequestDTO dto, ClaimsPrincipal caller, CancellationToken cancellationToken);
Task<AddUserOutcomeDTO> EditUserAsync(EditUserRequestDTO dto, ClaimsPrincipal caller, CancellationToken cancellationToken);
Task<bool> DeleteUserAsync(string id, CancellationToken cancellationToken);
Task<AddUserOutcomeDTO> DeleteUserAsync(string id, ClaimsPrincipal caller, CancellationToken cancellationToken);
Task DeleteCurrentUserAsync(string userId, CancellationToken cancellationToken);
Task<IReadOnlyList<UserProfileRowDTO>> GetUserProfileAsync(string userId, CancellationToken cancellationToken);
}