mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-06 12:22:08 +00:00
fix(uplifts): honor current permissions and denial contract
This commit is contained in:
parent
9156107e72
commit
ff6a5945f1
6 changed files with 115 additions and 14 deletions
|
|
@ -306,7 +306,37 @@ public class VendorPortalControllerTests
|
||||||
|
|
||||||
var result = await controller.RequestUplift(10, new VendorPortalController.UpliftRequestBody { RequestedNTE = 100m });
|
var result = await controller.RequestUplift(10, new VendorPortalController.UpliftRequestBody { RequestedNTE = 100m });
|
||||||
|
|
||||||
result.Should().BeOfType<BadRequestObjectResult>();
|
var badRequest = result.Should().BeOfType<BadRequestObjectResult>().Subject;
|
||||||
|
var response = badRequest.Value.Should().BeOfType<Response>().Subject;
|
||||||
|
response.Message.Should().Contain("reference");
|
||||||
|
response.Message.Should().NotContain("Requested NTE must be greater than the current NTE");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestUplift_Forbidden_Returns403WithExactPermissionMessage()
|
||||||
|
{
|
||||||
|
var service = new Mock<IVendorPortalService>();
|
||||||
|
service.Setup(x => x.ResolveSessionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(Session);
|
||||||
|
service.Setup(x => x.RequestUpliftAsync(
|
||||||
|
Session,
|
||||||
|
10,
|
||||||
|
500m,
|
||||||
|
It.IsAny<string?>(),
|
||||||
|
It.IsAny<string?>(),
|
||||||
|
It.IsAny<int?>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ThrowsAsync(new UpliftForbiddenException("internal permission detail"));
|
||||||
|
var controller = NewController(service);
|
||||||
|
|
||||||
|
var result = await controller.RequestUplift(
|
||||||
|
10,
|
||||||
|
new VendorPortalController.UpliftRequestBody { RequestedNTE = 500m });
|
||||||
|
|
||||||
|
var forbidden = result.Should().BeOfType<ObjectResult>().Subject;
|
||||||
|
forbidden.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
|
||||||
|
var response = forbidden.Value.Should().BeOfType<Response>().Subject;
|
||||||
|
response.Message.Should().Be("Your role can't request uplifts on this work order.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,16 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
{
|
{
|
||||||
return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
|
return NotFound(new Response { Status = "Error", Message = "Dispatch not found" });
|
||||||
}
|
}
|
||||||
|
catch (UpliftForbiddenException)
|
||||||
|
{
|
||||||
|
return StatusCode(
|
||||||
|
StatusCodes.Status403Forbidden,
|
||||||
|
new Response
|
||||||
|
{
|
||||||
|
Status = "Error",
|
||||||
|
Message = UpliftForbiddenException.RequestUpliftsDeniedMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex, "The requested action could not be completed for this dispatch") });
|
return BadRequest(new Response { Status = "Error", Message = _logger.Sanitize(ex, "The requested action could not be completed for this dispatch") });
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,9 @@ namespace SeaHaven.Services.DTOs
|
||||||
|
|
||||||
public class UpliftForbiddenException : Exception
|
public class UpliftForbiddenException : Exception
|
||||||
{
|
{
|
||||||
|
public const string RequestUpliftsDeniedMessage =
|
||||||
|
"Your role can't request uplifts on this work order.";
|
||||||
|
|
||||||
public UpliftForbiddenException(string message) : base(message) { }
|
public UpliftForbiddenException(string message) : base(message) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,17 +99,20 @@ namespace SeaHaven.Services.Implementation
|
||||||
var permissionUser = string.IsNullOrWhiteSpace(userId)
|
var permissionUser = string.IsNullOrWhiteSpace(userId)
|
||||||
? null
|
? null
|
||||||
: await _permissionOverrideData.GetUserAsync(userId, cancellationToken);
|
: await _permissionOverrideData.GetUserAsync(userId, cancellationToken);
|
||||||
var roleName = user.IsInRole("Admin")
|
|
||||||
? "Admin"
|
|
||||||
: user.FindFirstValue(ClaimTypes.Role);
|
|
||||||
|
|
||||||
if (!_permissionPolicy.IsAllowed(
|
if (permissionUser is null)
|
||||||
roleName,
|
|
||||||
TeamPermissionKeys.RequestUplifts,
|
|
||||||
permissionUser?.Overrides))
|
|
||||||
{
|
{
|
||||||
throw new UpliftForbiddenException(
|
throw new UpliftForbiddenException(
|
||||||
"Your role can't request uplifts on this work order.");
|
UpliftForbiddenException.RequestUpliftsDeniedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_permissionPolicy.IsAllowed(
|
||||||
|
permissionUser.RoleName,
|
||||||
|
TeamPermissionKeys.RequestUplifts,
|
||||||
|
permissionUser.Overrides))
|
||||||
|
{
|
||||||
|
throw new UpliftForbiddenException(
|
||||||
|
UpliftForbiddenException.RequestUpliftsDeniedMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SeaHaven.DataServices.Implementation;
|
using SeaHaven.DataServices.Implementation;
|
||||||
|
|
@ -304,6 +305,13 @@ public class WorkOrderBoardCancelServiceTests
|
||||||
FirstName = "Alex",
|
FirstName = "Alex",
|
||||||
LastName = "Dispatcher",
|
LastName = "Dispatcher",
|
||||||
});
|
});
|
||||||
|
var adminRole = new IdentityRole("Admin");
|
||||||
|
seed.Roles.Add(adminRole);
|
||||||
|
seed.UserRoles.Add(new IdentityUserRole<string>
|
||||||
|
{
|
||||||
|
UserId = "dispatcher-1",
|
||||||
|
RoleId = adminRole.Id,
|
||||||
|
});
|
||||||
seed.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
|
seed.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
|
||||||
seed.Dispatches.Add(new Dispatch
|
seed.Dispatches.Add(new Dispatch
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using SeaHaven.DataServices.Implementation;
|
using SeaHaven.DataServices.Implementation;
|
||||||
|
|
@ -49,7 +50,8 @@ public sealed class WorkOrderUpliftServiceTests
|
||||||
|
|
||||||
private static async Task<(WorkOrder WorkOrder, Dispatch Dispatch)> SeedWorkOrderAsync(
|
private static async Task<(WorkOrder WorkOrder, Dispatch Dispatch)> SeedWorkOrderAsync(
|
||||||
ApplicationDbContext context,
|
ApplicationDbContext context,
|
||||||
WorkOrderType type = WorkOrderType.PM)
|
WorkOrderType type = WorkOrderType.PM,
|
||||||
|
string role = "Admin")
|
||||||
{
|
{
|
||||||
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context);
|
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context);
|
||||||
context.Users.Add(new ApplicationUser
|
context.Users.Add(new ApplicationUser
|
||||||
|
|
@ -59,6 +61,13 @@ public sealed class WorkOrderUpliftServiceTests
|
||||||
FirstName = "Alex",
|
FirstName = "Alex",
|
||||||
LastName = "Dispatcher",
|
LastName = "Dispatcher",
|
||||||
});
|
});
|
||||||
|
var identityRole = new IdentityRole(role);
|
||||||
|
context.Roles.Add(identityRole);
|
||||||
|
context.UserRoles.Add(new IdentityUserRole<string>
|
||||||
|
{
|
||||||
|
UserId = "dispatcher-1",
|
||||||
|
RoleId = identityRole.Id,
|
||||||
|
});
|
||||||
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
|
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
|
||||||
context.Dispatches.Add(new Dispatch
|
context.Dispatches.Add(new Dispatch
|
||||||
{
|
{
|
||||||
|
|
@ -127,7 +136,7 @@ public sealed class WorkOrderUpliftServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateAsync_Admin_RemainsUnrestricted()
|
public async Task CreateAsync_CurrentDatabaseAdmin_RemainsUnrestricted()
|
||||||
{
|
{
|
||||||
await using var context = CreateContext();
|
await using var context = CreateContext();
|
||||||
var (workOrder, _) = await SeedWorkOrderAsync(context);
|
var (workOrder, _) = await SeedWorkOrderAsync(context);
|
||||||
|
|
@ -136,19 +145,57 @@ public sealed class WorkOrderUpliftServiceTests
|
||||||
var created = await service.CreateAsync(
|
var created = await service.CreateAsync(
|
||||||
workOrder.Id,
|
workOrder.Id,
|
||||||
new CreateWorkOrderUpliftRequestDto { Amount = 400m },
|
new CreateWorkOrderUpliftRequestDto { Amount = 400m },
|
||||||
Dispatcher(),
|
WorkOrderAccountTestHelpers.AccountUser("dispatcher-1", 1, "Scheduler"),
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
Assert.NotNull(created);
|
Assert.NotNull(created);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAsync_StaleAdminClaim_DeniesWhenDatabaseRoleIsNotAdmin()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var (workOrder, dispatch) = await SeedWorkOrderAsync(context, role: "Dispatcher");
|
||||||
|
var service = NewService(context);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<UpliftForbiddenException>(() => service.CreateAsync(
|
||||||
|
workOrder.Id,
|
||||||
|
new CreateWorkOrderUpliftRequestDto { Amount = 400m },
|
||||||
|
Dispatcher(),
|
||||||
|
CancellationToken.None));
|
||||||
|
|
||||||
|
Assert.Equal(UpliftForbiddenException.RequestUpliftsDeniedMessage, exception.Message);
|
||||||
|
Assert.Empty(context.DispatchUpliftRequests);
|
||||||
|
Assert.Equal(1000m, dispatch.NTEAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateAsync_MissingDatabaseUser_DeniesEvenWithAdminClaim()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var (workOrder, dispatch) = await SeedWorkOrderAsync(context);
|
||||||
|
context.Users.Remove(context.Users.Single(user => user.Id == "dispatcher-1"));
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
var service = NewService(context);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<UpliftForbiddenException>(() => service.CreateAsync(
|
||||||
|
workOrder.Id,
|
||||||
|
new CreateWorkOrderUpliftRequestDto { Amount = 400m },
|
||||||
|
Dispatcher("dispatcher-1"),
|
||||||
|
CancellationToken.None));
|
||||||
|
|
||||||
|
Assert.Equal(UpliftForbiddenException.RequestUpliftsDeniedMessage, exception.Message);
|
||||||
|
Assert.Empty(context.DispatchUpliftRequests);
|
||||||
|
Assert.Equal(1000m, dispatch.NTEAmount);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Dispatcher", true)]
|
[InlineData("Dispatcher", true)]
|
||||||
[InlineData("Scheduler", false)]
|
[InlineData("Scheduler", false)]
|
||||||
public async Task CreateAsync_UsesRoleDefaultForRequestPermission(string role, bool allowed)
|
public async Task CreateAsync_UsesRoleDefaultForRequestPermission(string role, bool allowed)
|
||||||
{
|
{
|
||||||
await using var context = CreateContext();
|
await using var context = CreateContext();
|
||||||
var (workOrder, dispatch) = await SeedWorkOrderAsync(context);
|
var (workOrder, dispatch) = await SeedWorkOrderAsync(context, role: role);
|
||||||
var service = NewService(context);
|
var service = NewService(context);
|
||||||
|
|
||||||
var act = () => service.CreateAsync(
|
var act = () => service.CreateAsync(
|
||||||
|
|
@ -178,7 +225,7 @@ public sealed class WorkOrderUpliftServiceTests
|
||||||
bool allowed)
|
bool allowed)
|
||||||
{
|
{
|
||||||
await using var context = CreateContext();
|
await using var context = CreateContext();
|
||||||
var (workOrder, dispatch) = await SeedWorkOrderAsync(context);
|
var (workOrder, dispatch) = await SeedWorkOrderAsync(context, role: role);
|
||||||
context.UserPermissionOverrides.Add(new UserPermissionOverride
|
context.UserPermissionOverrides.Add(new UserPermissionOverride
|
||||||
{
|
{
|
||||||
UserId = "dispatcher-1",
|
UserId = "dispatcher-1",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue