case 4720 - restricted roles get filtered work order items in report data, rendering and export

Opening a work order filters it for restricted users (TechRestricted,
SubContractor, SubContractorRestricted): only items they are scheduled on, and
on those only their own labour, travel and scheduling, and for some roles no
parts, units, expenses etc. Report data, rendering and export went through
WorkOrderGetPartialAsync, which filtered nothing for child types (security
review F27). Two causes:
- the partial fetch now applies the same filtering: the item only if the user
  is scheduled on it (checked in the database), then its details filtered; the
  existing per-item filtering moved unchanged into RemoveRestrictedUserItemDetails
  so both fetches share it;
- BizObjectFactory created WorkOrderBiz with UserType.Service for everyone, so
  in these paths a subcontractor was never treated as one; it now passes the
  user's real type (only the subcontractor flags depend on it).

Checked: report data for a restricted subcontractor on work order 1 went from
the whole item with parts to none; scheduled on an item, it gets the item
without parts or other users' labour. Full suite: 111 passed, 5 skipped, the
same 2 known failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 16:27:59 -07:00
parent ee59efff48
commit d254affc8b
4 changed files with 55 additions and 33 deletions

View File

@@ -1,3 +1,5 @@
using System.Linq;
using Microsoft.EntityFrameworkCore;
using AyaNova.Util;
using AyaNova.Models;
@@ -91,7 +93,9 @@ namespace AyaNova.Biz
case AyaType.WorkOrderItemTravel:
case AyaType.WorkOrderItemUnit:
case AyaType.WorkOrderItemOutsideService:
return new WorkOrderBiz(ct, userId, translationId, roles, UserType.Service);
//case 4720: the real user type, the subcontractor restrictions depend on it (report data and export come through here)
var workOrderUserType = ct.User.AsNoTracking().Where(z => z.Id == userId).Select(z => z.UserType).FirstOrDefault();
return new WorkOrderBiz(ct, userId, translationId, roles, workOrderUserType);

View File

@@ -222,6 +222,39 @@ namespace AyaNova.Biz
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
//Restricted users (tech restricted, subcontractors) see only their own labour, travel and scheduling on an item,
//and depending on the role no expenses, parts, units etc. (case 4720: shared by the full and the partial fetch)
private void RemoveRestrictedUserItemDetails(WorkOrderItem wi)
{
//all restricted types
wi.ScheduledUsers.RemoveAll(x => x.UserId != UserId);
wi.Labors.RemoveAll(x => x.UserId != UserId);
wi.Travels.RemoveAll(x => x.UserId != UserId);
if (UserIsTechRestricted)
{
wi.Expenses.RemoveAll(x => x.UserId != UserId);
}
if (UserIsSubContractorFull)
{
wi.Expenses.RemoveAll(x => true);
wi.OutsideServices.RemoveAll(x => true);
}
if (UserIsSubContractorRestricted)
{
wi.Units.RemoveAll(x => true);
wi.Parts.RemoveAll(x => true);
wi.PartRequests.RemoveAll(x => true);
wi.Expenses.RemoveAll(x => true);
wi.Loans.RemoveAll(x => true);
wi.OutsideServices.RemoveAll(x => true);
}
//tasks are allowed to be viewed and update the task completion types
}
internal async Task<WorkOrder> WorkOrderGetAsync(long id, bool populateDisplayFields, bool logTheGetEvent = true, bool populateForReporting = false)
{
@@ -263,37 +296,9 @@ namespace AyaNova.Biz
ret.IsCompleteRecord = false;
}
//Restricted users may have further restrictions
//Restricted users may have further restrictions
foreach (WorkOrderItem wi in ret.Items)
{
//all restricted types
wi.ScheduledUsers.RemoveAll(x => x.UserId != UserId);
wi.Labors.RemoveAll(x => x.UserId != UserId);
wi.Travels.RemoveAll(x => x.UserId != UserId);
if (userIsTechRestricted)
{
wi.Expenses.RemoveAll(x => x.UserId != UserId);
}
if (userIsSubContractorFull)
{
wi.Expenses.RemoveAll(x => true);
wi.OutsideServices.RemoveAll(x => true);
}
if (userIsSubContractorRestricted)
{
wi.Units.RemoveAll(x => true);
wi.Parts.RemoveAll(x => true);
wi.PartRequests.RemoveAll(x => true);
wi.Expenses.RemoveAll(x => true);
wi.Loans.RemoveAll(x => true);
wi.OutsideServices.RemoveAll(x => true);
}
//tasks are allowed to be viewed and update the task completion types
}
RemoveRestrictedUserItemDetails(wi);
}
if (populateDisplayFields)
@@ -994,6 +999,19 @@ namespace AyaNova.Biz
}
}
//case 4720: restricted users get the same filtering as in the full fetch: only an item they are scheduled on
//(checked in the database, the item here may have been loaded without its scheduled users), then its details filtered
if (woitem != null && UserIsRestrictedType)
{
if (!await ct.WorkOrderItemScheduledUser.AsNoTracking().AnyAsync(z => z.WorkOrderItemId == woitem.Id && z.UserId == UserId))
{
woitem = null;
ret.IsCompleteRecord = false;
}
else
RemoveRestrictedUserItemDetails(woitem);
}
if (woitem != null)
ret.Items.Add(woitem);