mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 16:33:12 +00:00
fix(work-orders): scope legacy GET GetComments by account [SH-221]
Pass ClaimsPrincipal into GetCommentsAsync and filter via GetAllForAccountAsync so account-scoped callers cannot enumerate cross-tenant comments. ADR + cross-account tests updated.
This commit is contained in:
parent
3c090e2757
commit
de0f6da8fb
7 changed files with 80 additions and 6 deletions
|
|
@ -370,8 +370,15 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
[Route("GetComments")]
|
||||
public async Task<IActionResult> GetComments()
|
||||
{
|
||||
var data = await _workOrderService.GetCommentsAsync();
|
||||
return Ok(data);
|
||||
try
|
||||
{
|
||||
var data = await _workOrderService.GetCommentsAsync(User);
|
||||
return Ok(data);
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "Forbidden")
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new Response { Status = "Error", Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Helpers;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
|
|
@ -25,6 +26,20 @@ namespace SeaHaven.DataServices.Implementation
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Comments>> GetAllForAccountAsync(int? accountId)
|
||||
{
|
||||
var workOrders = WorkOrderBoardQueryFilters.ApplyBaseScope(_context.workOrders.AsNoTracking());
|
||||
if (accountId.HasValue)
|
||||
workOrders = WorkOrderBoardQueryFilters.ApplyAccountScope(workOrders, accountId.Value);
|
||||
|
||||
return await (
|
||||
from c in _context.Comments.AsNoTracking().Include(c => c.ApplicationUser)
|
||||
join w in workOrders on c.WorkerOrderId equals w.Id
|
||||
orderby c.CreatedDate
|
||||
select c
|
||||
).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Comments>> GetByWorkOrderIdAsync(int workOrderId)
|
||||
{
|
||||
return await _context.Comments
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
{
|
||||
Task<Comments?> GetByIdAsync(int id);
|
||||
Task<IEnumerable<Comments>> GetAllAsync();
|
||||
/// <summary>
|
||||
/// Comments linked to non-deleted, non-template work orders.
|
||||
/// When <paramref name="accountId"/> is set, only that account's WOs; null = org-wide.
|
||||
/// </summary>
|
||||
Task<IEnumerable<Comments>> GetAllForAccountAsync(int? accountId);
|
||||
Task<IEnumerable<Comments>> GetByWorkOrderIdAsync(int workOrderId);
|
||||
Task<IEnumerable<Comments>> GetByDispatchIdAsync(int dispatchId);
|
||||
Task<Comments> AddAsync(Comments comment);
|
||||
|
|
|
|||
|
|
@ -486,9 +486,10 @@ namespace SeaHaven.Services.Implementation
|
|||
};
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CommentListItemReadModel>> GetCommentsAsync()
|
||||
public async Task<IEnumerable<CommentListItemReadModel>> GetCommentsAsync(ClaimsPrincipal user)
|
||||
{
|
||||
var comments = await _commentDataService.GetAllAsync();
|
||||
var accountId = _accountResolver.ResolveAccountFilter(user);
|
||||
var comments = await _commentDataService.GetAllForAccountAsync(accountId);
|
||||
return comments.Select(s => new CommentListItemReadModel
|
||||
{
|
||||
Id = s.Id,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ namespace SeaHaven.Services.Interfaces
|
|||
Task<bool> DeleteWorkOrderCascadeAsync(int id, string userId);
|
||||
Task<Comments> AddCommentAsync(AddCommentInput input, ClaimsPrincipal user, string userId);
|
||||
Task<CommentResult> AddCommentJsonAsync(AddCommentInput input, ClaimsPrincipal user, string userId);
|
||||
Task<IEnumerable<CommentListItemReadModel>> GetCommentsAsync();
|
||||
Task<IEnumerable<CommentListItemReadModel>> GetCommentsAsync(ClaimsPrincipal user);
|
||||
Task<IEnumerable<CommentListItemReadModel>?> GetCommentsByWorkorderIdAsync(int woid, ClaimsPrincipal user);
|
||||
Task<IEnumerable<WorkOrder>> GetWorkordersDDAsync(int? accountId = null);
|
||||
Task<IEnumerable<WorkorderFilterVM>> GetWorkordersAsync(int? accountId = null);
|
||||
|
|
|
|||
|
|
@ -636,4 +636,50 @@ public class WorkOrderAccountScopeTests
|
|||
Assert.Equal(DocStatus.Yes, result.DocStatus);
|
||||
Assert.Equal("https://example.com/ok.pdf", result.SignOffAttachment);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyGetComments_ScopedUser_HidesOtherAccountComments()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
await SeedThreeAccountRowsAsync(context);
|
||||
|
||||
context.Comments.AddRange(
|
||||
new Comments
|
||||
{
|
||||
WorkerOrderId = 1,
|
||||
Commenttext = "Account A note",
|
||||
Documents = "a.pdf",
|
||||
CreatedDate = DateTime.UtcNow
|
||||
},
|
||||
new Comments
|
||||
{
|
||||
WorkerOrderId = 2,
|
||||
Commenttext = "Account B secret",
|
||||
Documents = "b.pdf",
|
||||
CreatedDate = DateTime.UtcNow
|
||||
},
|
||||
new Comments
|
||||
{
|
||||
WorkerOrderId = 3,
|
||||
Commenttext = "Null account note",
|
||||
CreatedDate = DateTime.UtcNow
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var service = CreateLegacyReadService(context);
|
||||
var scoped = await service.GetCommentsAsync(
|
||||
WorkOrderAccountTestHelpers.AccountUser("disp-1", 1, "Dispatcher"));
|
||||
|
||||
Assert.Single(scoped);
|
||||
Assert.Equal("Account A note", scoped.Single().Commenttext);
|
||||
Assert.DoesNotContain(scoped, c => c.Commenttext == "Account B secret");
|
||||
Assert.DoesNotContain(scoped, c => c.Commenttext == "Null account note");
|
||||
|
||||
var orgWide = await service.GetCommentsAsync(WorkOrderAccountTestHelpers.OrgWideAdmin());
|
||||
Assert.Equal(3, orgWide.Count());
|
||||
|
||||
var missingEx = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
||||
service.GetCommentsAsync(WorkOrderAccountTestHelpers.MissingScope()));
|
||||
Assert.Equal("Forbidden", missingEx.Code);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
- JWT `account_id` when `ApplicationUser.AccountId` is set
|
||||
- JWT `org_scope=all` when Admin has no AccountId (explicit signed elevation)
|
||||
- **Reads** (board, list, advanced search, detail, media, board comments,
|
||||
legacy comments-by-work-order-id): `ApplyBaseScope` +
|
||||
legacy comments-by-work-order-id, legacy `GET GetComments`): `ApplyBaseScope` +
|
||||
`ApplyAccountScope(int)` when account-scoped; org-wide path skips account filter
|
||||
- **Writes** (board mutations, media, board/legacy comments, `POST …/completion-doc`):
|
||||
same account filter at service/data entry; authorize before storing blobs
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue