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}"); } } }