From d254affc8b28dfa1a8c0ee253c9f4bd46064d51d Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Sat, 24 Oct 2026 16:27:59 -0700 Subject: [PATCH] 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 --- devdocs/modernization/NET10-PORT.md | 2 +- devdocs/modernization/SECURITY-REVIEW.md | 2 +- server/AyaNova/biz/BizObjectFactory.cs | 6 +- server/AyaNova/biz/WorkOrderBiz.cs | 78 +++++++++++++++--------- 4 files changed, 55 insertions(+), 33 deletions(-) diff --git a/devdocs/modernization/NET10-PORT.md b/devdocs/modernization/NET10-PORT.md index ef5319dd..391e8071 100644 --- a/devdocs/modernization/NET10-PORT.md +++ b/devdocs/modernization/NET10-PORT.md @@ -186,7 +186,7 @@ John's "next release" cases for AyaNova (exported 2026-10-24). Each is its own s | 4717 | KPI interval allowlist | F26. **Done** (`KPIFetcher.DateTruncUnitOrNull` in the 7 KPIs reading `interval`; test `KpiInterval`) | | 4718 | Report and translation import need create rights | F19, F24. **Done** (contact and Service 403, BizAdmin 202; test `ImportRights`) | | 4719 | Report page network restricted; notification reports not run as SuperUser | F20 | -| 4720 | Restricted roles filtered in report/export data | F27 | +| 4720 | Restricted roles filtered in report/export data | F27. **Done** (partial fetch filtered like the full one; BizObjectFactory passed UserType.Service for everyone, now the real type; test `RestrictedReportData`) | | 4721 | Client escaping of record text | F33 | | 4722 | DOMPurify and Vuetify upgrades (raven-client) | F34 | | 4723 | Direct SMTP: address from the record only | F3, F4 | diff --git a/devdocs/modernization/SECURITY-REVIEW.md b/devdocs/modernization/SECURITY-REVIEW.md index 29260411..21b2027f 100644 --- a/devdocs/modernization/SECURITY-REVIEW.md +++ b/devdocs/modernization/SECURITY-REVIEW.md @@ -204,7 +204,7 @@ take the next free number; a fixed or accepted finding keeps its ID and changes | F16 | High | **fixed in case 4712** | `schedule/svc` and `schedule/user` have no role check: anyone logged in sees the whole service schedule | | F17 | Medium | **fixed in case 4712** | Notification subscriptions accept any `UserId`, and object events reach subscribers without a rights check: record names to any email address | | F20 | High | candidate | Report templates receive the renderer's token (SuperUser for customer notification reports) and the report browser can reach any network address | -| F27 | High | **confirmed** | Restricted roles (TechRestricted, SubContractor, SubContractorRestricted) get whole work-order items through report data, rendering and export | +| F27 | High | **fixed in case 4720** | Restricted roles (TechRestricted, SubContractor, SubContractorRestricted) get whole work-order items through report data, rendering and export | | F28 | High | **fixed in case 4713** | Users with change rights on Customer (Service, Sales, Accounting) can give customer contact logins any role, BizAdmin and OpsAdmin included | | F33 | High | confirmed (code) | Client renders some record text as HTML without escaping (search results, work-order address dialog, unit warranty info, dialogs) | | F3 | High | confirmed (code) | Any logged-in user, customer contacts included, can send arbitrary email through the company's SMTP server | diff --git a/server/AyaNova/biz/BizObjectFactory.cs b/server/AyaNova/biz/BizObjectFactory.cs index f3077886..11f814b0 100644 --- a/server/AyaNova/biz/BizObjectFactory.cs +++ b/server/AyaNova/biz/BizObjectFactory.cs @@ -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); diff --git a/server/AyaNova/biz/WorkOrderBiz.cs b/server/AyaNova/biz/WorkOrderBiz.cs index c3dc57c9..f61c6c88 100644 --- a/server/AyaNova/biz/WorkOrderBiz.cs +++ b/server/AyaNova/biz/WorkOrderBiz.cs @@ -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 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);