[CLAUDE] PurchaseEvaluation: Plan E — phân quyền strict V2 scope (List + Detail)
Thắt chặt phân quyền PE V2 từ UAT loose sang strict actor.UserId scope: Trước (loose): mọi authenticated user thấy mọi phiếu V2 (`ApprovalWorkflowId != null`). Sau (strict): - ListPurchaseEvaluationsQuery: phiếu V2 chỉ visible khi actor là approver trong any Step.Level.ApproverUserId của workflow đã pin. Pre-compute userApprovalWfIds = DISTINCT workflow IDs có user trong Levels. - GetPurchaseEvaluationQuery: same — actor must be V2 approver in any Level của workflow pin để thấy phiếu (ngoài Drafter scope + role eligible phase). Drafter vẫn thấy phiếu mình tạo (regardless V2/V1). Admin bypass full. Inbox đã strict từ Session 17 (ResolveV2InboxIdsAsync match current Cấp + ApproverUserId) — KHÔNG đụng. Tests defer: Plan C carry — 4 integration tests Strict V2 List + Detail (Drafter own / V2 approver / non-approver throw 403) khi UAT confirm. Verify: - dotnet build SolutionErp.slnx — 0 err, 2 warning DocxRenderer pre-existing - dotnet test SolutionErp.slnx — 103/103 PASS regression-free Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -7,6 +7,7 @@ using SolutionErp.Application.Common.Interfaces;
|
|||||||
using SolutionErp.Application.Common.Models;
|
using SolutionErp.Application.Common.Models;
|
||||||
using SolutionErp.Application.PurchaseEvaluations.Dtos;
|
using SolutionErp.Application.PurchaseEvaluations.Dtos;
|
||||||
using SolutionErp.Application.PurchaseEvaluations.Services;
|
using SolutionErp.Application.PurchaseEvaluations.Services;
|
||||||
|
using SolutionErp.Domain.ApprovalWorkflowsV2; // Plan E V2 strict scope query
|
||||||
using SolutionErp.Domain.Contracts;
|
using SolutionErp.Domain.Contracts;
|
||||||
using SolutionErp.Domain.Identity;
|
using SolutionErp.Domain.Identity;
|
||||||
using SolutionErp.Domain.PurchaseEvaluations;
|
using SolutionErp.Domain.PurchaseEvaluations;
|
||||||
@ -315,18 +316,28 @@ public class ListPurchaseEvaluationsQueryHandler(
|
|||||||
from s in sj.DefaultIfEmpty()
|
from s in sj.DefaultIfEmpty()
|
||||||
select new { e, p, s };
|
select new { e, p, s };
|
||||||
|
|
||||||
// IDOR: non-admin chỉ thấy phiếu mình là Drafter hoặc role eligible phase.
|
// IDOR strict (Plan E S22 — Session 21 +1): non-admin chỉ thấy phiếu khi:
|
||||||
// [Session 17 UAT loose] Phiếu pin V2 (ApprovalWorkflowId != null) →
|
// 1. là Drafter (mình tạo)
|
||||||
// tạm cho mọi authenticated user thấy để UAT (sẽ thắt chặt sau khi
|
// 2. role eligible cho phase legacy V1 (eligiblePhases)
|
||||||
// hoàn thiện logic permission V2-aware).
|
// 3. V2 approver: pin workflow có actor.UserId trong any Step.Level.ApproverUserId
|
||||||
|
// Trước đây UAT loose `|| x.e.ApprovalWorkflowId != null` cho mọi
|
||||||
|
// authenticated thấy phiếu V2 — đã thắt chặt sau UAT confirm flow ổn.
|
||||||
if (!currentUser.Roles.Contains(AppRoles.Admin))
|
if (!currentUser.Roles.Contains(AppRoles.Admin))
|
||||||
{
|
{
|
||||||
var userId = currentUser.UserId;
|
var userId = currentUser.UserId;
|
||||||
var eligiblePhases = GetEligiblePhases(currentUser.Roles);
|
var eligiblePhases = GetEligiblePhases(currentUser.Roles);
|
||||||
|
// Pre-compute V2 workflow IDs nơi user là approver trong any Step.Level
|
||||||
|
var userApprovalWfIds = userId is null
|
||||||
|
? new List<Guid>()
|
||||||
|
: await db.ApprovalWorkflowLevels.AsNoTracking()
|
||||||
|
.Where(l => l.ApproverUserId == userId.Value)
|
||||||
|
.Select(l => l.Step!.ApprovalWorkflowId)
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync(ct);
|
||||||
q = q.Where(x =>
|
q = q.Where(x =>
|
||||||
x.e.DrafterUserId == userId
|
x.e.DrafterUserId == userId
|
||||||
|| eligiblePhases.Contains(x.e.Phase)
|
|| eligiblePhases.Contains(x.e.Phase)
|
||||||
|| x.e.ApprovalWorkflowId != null); // V2 loose UAT
|
|| (x.e.ApprovalWorkflowId != null && userApprovalWfIds.Contains(x.e.ApprovalWorkflowId.Value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Type is not null) q = q.Where(x => x.e.Type == request.Type);
|
if (request.Type is not null) q = q.Where(x => x.e.Type == request.Type);
|
||||||
@ -501,10 +512,18 @@ public class GetPurchaseEvaluationQueryHandler(
|
|||||||
{
|
{
|
||||||
var isDrafter = e.DrafterUserId == currentUser.UserId;
|
var isDrafter = e.DrafterUserId == currentUser.UserId;
|
||||||
var eligiblePhases = ListPurchaseEvaluationsQueryHandler.GetEligiblePhases(currentUser.Roles);
|
var eligiblePhases = ListPurchaseEvaluationsQueryHandler.GetEligiblePhases(currentUser.Roles);
|
||||||
// [Session 17 UAT loose] Phiếu pin V2 → cho mọi authenticated user
|
// V2 strict scope (Plan E S22 — Session 21 +1): actor là approver
|
||||||
// xem được (sẽ thắt chặt sau khi user UAT confirm flow).
|
// trong any Step.Level của workflow đã pin. Trước đây loose
|
||||||
var isPinnedV2 = e.ApprovalWorkflowId is not null;
|
// `isPinnedV2 = e.ApprovalWorkflowId is not null` đã thắt chặt sau
|
||||||
if (!isDrafter && !eligiblePhases.Contains(e.Phase) && !isPinnedV2)
|
// UAT confirm flow V2.
|
||||||
|
var isV2Approver = false;
|
||||||
|
if (e.ApprovalWorkflowId is Guid awIdForCheck && currentUser.UserId is Guid uidForCheck)
|
||||||
|
{
|
||||||
|
isV2Approver = await db.ApprovalWorkflowLevels.AsNoTracking()
|
||||||
|
.AnyAsync(l => l.Step!.ApprovalWorkflowId == awIdForCheck
|
||||||
|
&& l.ApproverUserId == uidForCheck, ct);
|
||||||
|
}
|
||||||
|
if (!isDrafter && !eligiblePhases.Contains(e.Phase) && !isV2Approver)
|
||||||
throw new ForbiddenException("Bạn không có quyền xem phiếu này.");
|
throw new ForbiddenException("Bạn không có quyền xem phiếu này.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user