A password set by someone else (reset code, administrator) or a disabled login replaces the user's session token and download key; changing one's own password keeps the session. PasswordResetCodeExpire is cleared (was DlKeyExpire). The token middleware exempts only the exact login and notify/hello routes and refuses inactive users and users who may not log in. With a purchased licence, notify/hello no longer offers the default SuperUser login and a SuperUser with the default password may only change it (SuperUserDefaultPasswordRoutes). F7, F8 and F9 fixed. Manual and changelog updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
108 lines
4.5 KiB
C#
108 lines
4.5 KiB
C#
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;
|
|
var key = RouteKey(context);
|
|
return key != null && Allowed.Contains(key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// "METHOD route template" of the endpoint this request was routed to, API prefix removed
|
|
/// (e.g. "GET user-option/{id}"); null when not routed to a route endpoint.
|
|
/// Also used by the token middleware (case 4725)
|
|
/// </summary>
|
|
internal static string RouteKey(HttpContext context)
|
|
{
|
|
if (context.GetEndpoint() is not RouteEndpoint routeEndpoint)
|
|
return null;
|
|
var template = routeEndpoint.RoutePattern.RawText ?? string.Empty;
|
|
if (template.StartsWith(API_PREFIX))
|
|
template = template.Substring(API_PREFIX.Length);
|
|
return $"{context.Request.Method.ToUpperInvariant()} {template}";
|
|
}
|
|
}
|
|
}
|