case 4712 - customer contacts' pick lists limited to their own records

Step 4 of 4712 (security review F15). Pick lists checked only the Select role,
which customer contacts have for most types, and took the columns from the
request, so a contact could list every customer with phones and emails. For
contacts now: the customer list holds only their own customer (head office
contacts: their head office's customers); the unit list only their own
customer's units (head office contacts: the chosen customer, which must be
theirs; none until one is chosen); unit models as before; any other list 403.
pick-list/single names only those records. New CustomerPickList variant for a
single customer. Staff unchanged.

Checked: the contact's customer list went from 72 customers to its own one;
units only its own; vendors and users 403.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 14:14:43 -07:00
parent 692fececc7
commit 443d382394
4 changed files with 78 additions and 4 deletions

View File

@@ -75,6 +75,13 @@ namespace AyaNova.Api.Controllers
return StatusCode(403, new ApiNotAuthorizedResponse());
//case 4712: customer contacts get only the pick lists of the service request screen, limited to their own records
var contactVariant = await ContactPickListVariantAsync(pickListParams.AyaType, pickListParams.ListVariant);
if (contactVariant == CONTACT_DENIED)
return StatusCode(403, new ApiNotAuthorizedResponse());
if (contactVariant != null)
pickListParams.ListVariant = contactVariant;
//Instantiate the business object handler
PickListBiz biz = PickListBiz.GetBiz(ct, HttpContext);
@@ -122,6 +129,10 @@ namespace AyaNova.Api.Controllers
if (!Authorized.HasSelectRole(HttpContext.Items, pickListSingleParams.AyaType))
return StatusCode(403, new ApiNotAuthorizedResponse());
//case 4712: a customer contact gets names only of its own customers, their units, and unit models
if (!await ContactMayNameRecordAsync(pickListSingleParams.AyaType, pickListSingleParams.Id))
return StatusCode(403, new ApiNotAuthorizedResponse());
//Instantiate the business object handler
PickListBiz biz = PickListBiz.GetBiz(ct, HttpContext);
@@ -276,9 +287,68 @@ namespace AyaNova.Api.Controllers
}
//case 4712: marker for a pick list a customer contact may not use
private const string CONTACT_DENIED = "\u0000contact-denied";
//case 4712: for a customer contact, the variant that limits a pick list to its own records:
//null when the caller is not a contact (request used as is), CONTACT_DENIED for any list the
//customer screens do not use or a customer the contact does not own
private async Task<string> ContactPickListVariantAsync(AyaType aType, string requestedVariant)
{
var userType = UserTypeFromContext.Type(HttpContext.Items);
if (userType != UserType.Customer && userType != UserType.HeadOffice)
return null;
var userId = UserIdFromContext.Id(HttpContext.Items);
var u = await ct.User.AsNoTracking().Where(x => x.Id == userId).Select(x => new { x.CustomerId, x.HeadOfficeId }).SingleOrDefaultAsync();
long ownCustomerId = u?.CustomerId ?? 0;
long ownHeadOfficeId = u?.HeadOfficeId ?? 0;
switch (aType)
{
case AyaType.Customer:
if (userType == UserType.Customer)
return ownCustomerId == 0 ? CONTACT_DENIED : $"{ownCustomerId},{(int)AyaType.Customer}";
return ownHeadOfficeId == 0 ? CONTACT_DENIED : $"{ownHeadOfficeId},{(int)AyaType.HeadOffice}";
case AyaType.Unit:
if (userType == UserType.Customer)
return ownCustomerId == 0 ? CONTACT_DENIED : $"customerid:{ownCustomerId}";
//head office contact: units of the customer chosen on the form, which must be one of its customers;
//none until a customer is chosen
var v = (requestedVariant ?? string.Empty).Split(':');
long requestedCustomerId = 0;
if (v.Length == 2 && v[0].Trim() == "customerid")
long.TryParse(v[1].Trim(), System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out requestedCustomerId);
if (requestedCustomerId == 0)
return "customerid:-1";
return await UserBiz.CustomerContactOwnsCustomerAsync(ct, userId, requestedCustomerId) ? $"customerid:{requestedCustomerId}" : CONTACT_DENIED;
case AyaType.UnitModel:
return requestedVariant;//unit models are not customer data
default:
return CONTACT_DENIED;
}
}
//case 4712: may the caller see this record's pick list name (always true for staff)
private async Task<bool> ContactMayNameRecordAsync(AyaType aType, long id)
{
var userType = UserTypeFromContext.Type(HttpContext.Items);
if (userType != UserType.Customer && userType != UserType.HeadOffice)
return true;
var userId = UserIdFromContext.Id(HttpContext.Items);
switch (aType)
{
case AyaType.Customer:
return id == 0 || await UserBiz.CustomerContactOwnsCustomerAsync(ct, userId, id);
case AyaType.Unit:
if (id == 0)
return true;
var unitCustomerId = await ct.Unit.AsNoTracking().Where(x => x.Id == id).Select(x => (long?)x.CustomerId).FirstOrDefaultAsync();
return unitCustomerId != null && await UserBiz.CustomerContactOwnsCustomerAsync(ct, userId, (long)unitCustomerId);
case AyaType.UnitModel:
return true;
default:
return false;
}
}
}//eoc
}//ens