using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace AyaNova.Api.ControllerHelpers
{
///
/// 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".
///
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 Allowed = new HashSet
{
//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",
};
///
/// 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.
///
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() != 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}");
}
}
}