mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-02 22:43:31 +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")]
|
[Route("GetComments")]
|
||||||
public async Task<IActionResult> GetComments()
|
public async Task<IActionResult> GetComments()
|
||||||
{
|
{
|
||||||
var data = await _workOrderService.GetCommentsAsync();
|
try
|
||||||
return Ok(data);
|
{
|
||||||
|
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]
|
[HttpGet]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Helpers;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Implementation
|
namespace SeaHaven.DataServices.Implementation
|
||||||
|
|
@ -25,6 +26,20 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
.ToListAsync();
|
.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)
|
public async Task<IEnumerable<Comments>> GetByWorkOrderIdAsync(int workOrderId)
|
||||||
{
|
{
|
||||||
return await _context.Comments
|
return await _context.Comments
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
{
|
{
|
||||||
Task<Comments?> GetByIdAsync(int id);
|
Task<Comments?> GetByIdAsync(int id);
|
||||||
Task<IEnumerable<Comments>> GetAllAsync();
|
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>> GetByWorkOrderIdAsync(int workOrderId);
|
||||||
Task<IEnumerable<Comments>> GetByDispatchIdAsync(int dispatchId);
|
Task<IEnumerable<Comments>> GetByDispatchIdAsync(int dispatchId);
|
||||||
Task<Comments> AddAsync(Comments comment);
|
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
|
return comments.Select(s => new CommentListItemReadModel
|
||||||
{
|
{
|
||||||
Id = s.Id,
|
Id = s.Id,
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ namespace SeaHaven.Services.Interfaces
|
||||||
Task<bool> DeleteWorkOrderCascadeAsync(int id, string userId);
|
Task<bool> DeleteWorkOrderCascadeAsync(int id, string userId);
|
||||||
Task<Comments> AddCommentAsync(AddCommentInput input, ClaimsPrincipal user, string userId);
|
Task<Comments> AddCommentAsync(AddCommentInput input, ClaimsPrincipal user, string userId);
|
||||||
Task<CommentResult> AddCommentJsonAsync(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<CommentListItemReadModel>?> GetCommentsByWorkorderIdAsync(int woid, ClaimsPrincipal user);
|
||||||
Task<IEnumerable<WorkOrder>> GetWorkordersDDAsync(int? accountId = null);
|
Task<IEnumerable<WorkOrder>> GetWorkordersDDAsync(int? accountId = null);
|
||||||
Task<IEnumerable<WorkorderFilterVM>> GetWorkordersAsync(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(DocStatus.Yes, result.DocStatus);
|
||||||
Assert.Equal("https://example.com/ok.pdf", result.SignOffAttachment);
|
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 `account_id` when `ApplicationUser.AccountId` is set
|
||||||
- JWT `org_scope=all` when Admin has no AccountId (explicit signed elevation)
|
- JWT `org_scope=all` when Admin has no AccountId (explicit signed elevation)
|
||||||
- **Reads** (board, list, advanced search, detail, media, board comments,
|
- **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
|
`ApplyAccountScope(int)` when account-scoped; org-wide path skips account filter
|
||||||
- **Writes** (board mutations, media, board/legacy comments, `POST …/completion-doc`):
|
- **Writes** (board mutations, media, board/legacy comments, `POST …/completion-doc`):
|
||||||
same account filter at service/data entry; authorize before storing blobs
|
same account filter at service/data entry; authorize before storing blobs
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue