case 4713 - customer contacts hold and act with customer roles only

Users who may edit contacts but not users (Service, Sales, Accounting) could
give a contact login any role, BizAdmin and OpsAdmin included, and roles are
what every check uses (security review F28). Now:
- saving a Customer or HeadOffice user with any role other than Customer or
  CustomerRestricted is refused (error on Roles);
- UserBiz.EffectiveRoles masks a contact's roles to the customer roles wherever
  they are applied: the token middleware, download-key routes, and the
  notification subscriber check, so contacts that already have staff roles
  stored act without them.

Checked: Service creating a contact with BizAdmin refused, with Customer works;
a contact with OpsAdmin stored in the database gets 403 downloading a log file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 15:59:35 -07:00
parent 6edb57c749
commit ee59efff48
5 changed files with 24 additions and 5 deletions

View File

@@ -406,7 +406,8 @@ namespace AyaNova
//get the user record
var u = await ct.User.AsNoTracking().Where(a => a.Id == userId).Select(m => new { roles = m.Roles, name = m.Name, m.UserType, id = m.Id, translationId = m.UserOptions.TranslationId, currentAuthToken = m.CurrentAuthToken }).FirstAsync();
context.Request.HttpContext.Items["AY_ROLES"] = u.roles;
//case 4713: a customer contact acts only with the customer roles, whatever roles are stored
context.Request.HttpContext.Items["AY_ROLES"] = UserBiz.EffectiveRoles(u.UserType, u.roles);
context.Request.HttpContext.Items["AY_USERNAME"] = u.name;
context.Request.HttpContext.Items["AY_USER_ID"] = u.id;
context.Request.HttpContext.Items["AY_TRANSLATION_ID"] = u.translationId;

View File

@@ -75,8 +75,8 @@ namespace AyaNova.Biz
//case 4712: object events (created, modified, deleted, age) go only to subscribers with read rights on that type
private static async Task<bool> SubscriberMayReadAsync(AyContext ct, long userId, AyaType aType)
{
var roles = await ct.User.AsNoTracking().Where(z => z.Id == userId).Select(z => z.Roles).FirstOrDefaultAsync();
return AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole(roles, aType);
var u = await ct.User.AsNoTracking().Where(z => z.Id == userId).Select(z => new { z.UserType, z.Roles }).FirstOrDefaultAsync();
return u != null && AyaNova.Api.ControllerHelpers.Authorized.HasReadFullRole(UserBiz.EffectiveRoles(u.UserType, u.Roles), aType);
}
public static async Task ProcessStandardObjectCreatedEvents(ICoreBizObjectModel newObject, AyContext ct)

View File

@@ -185,6 +185,14 @@ namespace AyaNova.Biz
}
}
//case 4713: the roles a user acts with: customer contacts only ever the customer roles, whatever is stored
internal static AuthorizationRoles EffectiveRoles(UserType userType, AuthorizationRoles storedRoles)
{
if (userType == UserType.Customer || userType == UserType.HeadOffice)
return storedRoles & (AuthorizationRoles.Customer | AuthorizationRoles.CustomerRestricted);
return storedRoles;
}
//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)
@@ -1035,6 +1043,14 @@ namespace AyaNova.Biz
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Roles");
}
//case 4713: customer contacts may hold only the customer roles (users who may edit contacts
//but not users, such as Sales, could otherwise give a contact login administrator roles)
if ((proposedObj.UserType == UserType.Customer || proposedObj.UserType == UserType.HeadOffice)
&& EffectiveRoles(proposedObj.UserType, proposedObj.Roles) != proposedObj.Roles)
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Roles");
}
//Any form customizations to validate?
//contact is type if customer user
@@ -1146,6 +1162,8 @@ namespace AyaNova.Biz
var utcNow = new DateTimeOffset(DateTime.Now.ToUniversalTime(), TimeSpan.Zero);
if (DownloadUser.DlKeyExpire < utcNow.DateTime)
return null;
//case 4713: download routes check these roles, so a customer contact gets only the customer roles here too
DownloadUser.Roles = EffectiveRoles(DownloadUser.UserType, DownloadUser.Roles);
return DownloadUser;
}