case 4712 - customer contacts limited to the routes the customer screens use
Customer contacts had the Customer role's Select and read rights on most record types and could call any route whose staff role check they passed, including several with no role check (security review F13 to F17). Step 1 of 4712: the token middleware now answers 403 (same body as other 403s) to Customer and HeadOffice users for any route not in CustomerContactRoutes, the routes the customer screens use (read from raven-client). Anonymous routes are always allowed. New routes stay closed to contacts unless added on purpose. Ownership of the records these routes return follows in the next steps. Checked: full integration suite 103 passed, 5 skipped, the same 2 known failures as the baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
96
server/AyaNova/ControllerHelpers/CustomerContactRoutes.cs
Normal file
96
server/AyaNova/ControllerHelpers/CustomerContactRoutes.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace AyaNova.Api.ControllerHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// case 4712: the only API routes a customer contact (UserType Customer or HeadOffice) may call:
|
||||
/// the ones the customer screens of the client use. Everything else answers 403 to a contact, so a
|
||||
/// new route stays closed to contacts unless it is added here on purpose.
|
||||
/// Ownership of the records these routes return is checked in the routes themselves.
|
||||
/// See devdocs/modernization/SECURITY-REVIEW.md, "Customer contact route allowlist".
|
||||
/// </summary>
|
||||
internal static class CustomerContactRoutes
|
||||
{
|
||||
private const string API_PREFIX = "api/v{version:apiVersion}/";
|
||||
|
||||
//"METHOD route template" with the API prefix removed, exactly as in the controllers' attributes
|
||||
private static readonly HashSet<string> Allowed = new HashSet<string>
|
||||
{
|
||||
//own account
|
||||
"POST auth/change-password",
|
||||
"GET auth/totp",
|
||||
"POST auth/totp-validate",
|
||||
"POST auth/totp-disable/{id}",
|
||||
"GET user-option/{id}",
|
||||
"PUT user-option/{id}",
|
||||
//app shell
|
||||
"GET global-biz-setting/client",
|
||||
"GET enum-list/list/{enumkey}",
|
||||
"GET form-custom/{formkey}",
|
||||
"GET translation/list",
|
||||
"POST translation/subset",
|
||||
"GET server-info",
|
||||
"GET notify/new-count",
|
||||
"GET notify/app-notifications",
|
||||
"DELETE notify/{id}",
|
||||
//data lists (the server sets the customer filter for contacts)
|
||||
"POST data-list",
|
||||
"GET data-list/listfields",
|
||||
"GET data-list-column-view/{listKey}",
|
||||
"POST data-list-column-view",
|
||||
"DELETE data-list-column-view/{listKey}",
|
||||
"POST data-list-column-view/sort",
|
||||
"GET data-list-filter/list",
|
||||
"GET data-list-filter/{id}",
|
||||
"POST data-list-filter",
|
||||
"PUT data-list-filter",
|
||||
"DELETE data-list-filter/{id}",
|
||||
"GET tag-list/list",
|
||||
"POST pick-list/list",
|
||||
"POST pick-list/single",
|
||||
//work order (customer view, customer report, attachments)
|
||||
"GET workorder/customer/{id}",
|
||||
"GET attachment/list",
|
||||
"POST report/render-job",
|
||||
"GET job-operations/status/{gid}",
|
||||
"GET job-operations/logs/{gid}",
|
||||
"POST report/request-cancel/{gid}",
|
||||
//customer service requests (not accept / reject, which are staff actions)
|
||||
"POST csr",
|
||||
"PUT csr",
|
||||
"GET csr/{id}",
|
||||
"DELETE csr/{id}",
|
||||
"POST unit/customerunit",
|
||||
//notification subscriptions
|
||||
"GET notify-subscription/list",
|
||||
"GET notify-subscription/{id}",
|
||||
"POST notify-subscription",
|
||||
"PUT notify-subscription",
|
||||
"DELETE notify-subscription/{id}",
|
||||
"GET work-order-status/list",
|
||||
"GET quote-status/list",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// True if a customer contact may call the endpoint this request was routed to.
|
||||
/// Anonymous routes (login, downloads by key, logo...) are public anyway and always allowed.
|
||||
/// </summary>
|
||||
internal static bool IsAllowed(HttpContext context)
|
||||
{
|
||||
var endpoint = context.GetEndpoint();
|
||||
if (endpoint == null)
|
||||
return true;//not routed to anything: the request ends in a 404 regardless
|
||||
if (endpoint.Metadata.GetMetadata<IAllowAnonymous>() != null)
|
||||
return true;
|
||||
if (endpoint is not RouteEndpoint routeEndpoint)
|
||||
return false;
|
||||
var template = routeEndpoint.RoutePattern.RawText ?? string.Empty;
|
||||
if (template.StartsWith(API_PREFIX))
|
||||
template = template.Substring(API_PREFIX.Length);
|
||||
return Allowed.Contains($"{context.Request.Method.ToUpperInvariant()} {template}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user