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);