case 4712 - customer contacts reach only their own customer's work orders

Step 2 of 4712 (security review F13). The customer work-order rights checked
only tag-based feature switches, never which customer a work order belongs to.
New UserBiz.CustomerContactOwnsCustomerAsync (own customer, or for a head
office contact a customer of its head office), used by:
- workorder/customer/{id}: another customer's work order is not found (404);
- customer work order report (-100): exactly one work order, which must be the
  contact's (the renderer used every id it was given);
- work order attachment list and download for contacts; other attachment types
  are refused to contacts (the customer screens show them only on work orders).

Checked: other customer's work order 404 (was 200); report on another's or on
two work orders not found; own customer report still accepted (202).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 13:52:31 -07:00
parent 2b1adaa496
commit bd4494abac
6 changed files with 48 additions and 11 deletions

View File

@@ -157,11 +157,16 @@ namespace AyaNova.Api.Controllers
//Is this a customer user attempting to view a wo attachments??
var userType = UserTypeFromContext.Type(HttpContext.Items);
if (ayaType == AyaType.WorkOrder && (userType == UserType.Customer || userType == UserType.HeadOffice))
//case 4712: customer contacts see attachments only on their own customer's work orders
if (userType == UserType.Customer || userType == UserType.HeadOffice)
{
if (ayaType != AyaType.WorkOrder)
return StatusCode(403, new ApiNotAuthorizedResponse());
var wo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == ayaId).Select(x => new { x.Tags, x.CustomerId }).FirstOrDefaultAsync();
if (wo == null || !await UserBiz.CustomerContactOwnsCustomerAsync(ct, UserIdFromContext.Id(HttpContext.Items), wo.CustomerId))
return StatusCode(403, new ApiNotAuthorizedResponse());
//check if allowed
var woTags = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == ayaId).Select(x => x.Tags).FirstOrDefaultAsync();
var custUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(UserIdFromContext.Id(HttpContext.Items), woTags);
var custUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(UserIdFromContext.Id(HttpContext.Items), wo.Tags);
if (!custUserRights.ThisWOCanAttachments)
return StatusCode(403, new ApiNotAuthorizedResponse());
}
@@ -553,11 +558,16 @@ namespace AyaNova.Api.Controllers
//is this allowed?
//Is this a customer user attempting to view a wo attachments??
if (dbObject.AttachToAType == AyaType.WorkOrder && (DownloadUser.UserType == UserType.Customer || DownloadUser.UserType == UserType.HeadOffice))
//case 4712: customer contacts download attachments only from their own customer's work orders
if (DownloadUser.UserType == UserType.Customer || DownloadUser.UserType == UserType.HeadOffice)
{
if (dbObject.AttachToAType != AyaType.WorkOrder)
return StatusCode(403, new ApiNotAuthorizedResponse());
var wo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == dbObject.AttachToObjectId).Select(x => new { x.Tags, x.CustomerId }).FirstOrDefaultAsync();
if (wo == null || !await UserBiz.CustomerContactOwnsCustomerAsync(ct, DownloadUser.Id, wo.CustomerId))
return StatusCode(403, new ApiNotAuthorizedResponse());
//check if allowed
var woTags = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == dbObject.AttachToObjectId).Select(x => x.Tags).FirstOrDefaultAsync();
var custUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(DownloadUser.Id, woTags);
var custUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(DownloadUser.Id, wo.Tags);
if (!custUserRights.ThisWOCanAttachments)
return StatusCode(403, new ApiNotAuthorizedResponse());
}

View File

@@ -581,13 +581,24 @@ What could be the issue here?
{
log.LogDebug("customer user report requested");
//get the user and workorder data and set the actual report id or return null if not found
var woTags = await ct
//case 4712: exactly one work order, and it must belong to the contact's customer (the render
//below uses every id in SelectedRowIds, so extra ids are not passed on)
if (reportRequest.SelectedRowIds == null || reportRequest.SelectedRowIds.Length != 1)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
var wo = await ct
.WorkOrder.AsNoTracking()
.Where(x => x.Id == reportRequest.SelectedRowIds[0])
.Select(x => x.Tags)
.Select(x => new { x.Tags, x.CustomerId })
.FirstOrDefaultAsync();
if (woTags == null)
if (wo == null || !await UserBiz.CustomerContactOwnsCustomerAsync(ct, UserId, wo.CustomerId))
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
var woTags = wo.Tags;
var cr = await UserBiz.CustomerUserEffectiveRightsAsync(UserId, woTags);
if (cr.ThisWOEffectiveWOReportId == null)
return null;

View File

@@ -185,6 +185,20 @@ namespace AyaNova.Biz
}
}
//case 4712: true if the customer is a customer contact's own customer or, for a head office contact,
//one of its head office's customers; false for any other user type
internal static async Task<bool> CustomerContactOwnsCustomerAsync(AyContext ct, long userId, long customerId)
{
var u = await ct.User.AsNoTracking().Where(x => x.Id == userId).Select(x => new { x.UserType, x.CustomerId, x.HeadOfficeId }).FirstOrDefaultAsync();
if (u == null)
return false;
if (u.UserType == UserType.Customer)
return u.CustomerId != null && u.CustomerId == customerId;
if (u.UserType == UserType.HeadOffice)
return u.HeadOfficeId != null && await ct.Customer.AsNoTracking().AnyAsync(x => x.Id == customerId && x.HeadOfficeId == u.HeadOfficeId);
return false;
}
//For auth and access in client, also when opening wo and also when reporting wo
internal static async Task<CustomerRightsRecord> CustomerUserEffectiveRightsAsync(long userId, List<string> thisWorkOrderTags = null)
{

View File

@@ -314,6 +314,8 @@ namespace AyaNova.Biz
{
var sourceWo = await ct.WorkOrder.AsNoTracking().FirstOrDefaultAsync(z => z.Id == id);
if (sourceWo == null) return null;
//case 4712: another customer's work order is answered as not found
if (!await UserBiz.CustomerContactOwnsCustomerAsync(ct, UserId, sourceWo.CustomerId)) return null;
var customerEffectiveRights = await UserBiz.CustomerUserEffectiveRightsAsync(UserId, sourceWo.Tags);
var ret = new CustomerWorkOrder();
ret.Id = sourceWo.Id;