Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@
"Bash(brew --prefix dotnet)",
"Bash(/opt/homebrew/opt/dotnet/bin/dotnet build:*)",
"Bash(brew info:*)",
"mcp__graperoot-pro__graph_register_edit"
"mcp__graperoot-pro__graph_register_edit",
"mcp__graperoot-pro__graph_grep_all",
"Bash(/usr/local/share/dotnet/dotnet build *)",
"Bash(awk 'NR>=94 && /HttpGet\\\\\\(\"IncomingMessage\"\\\\\\)/{f=1} f{print NR\": \"$0} f && /^\\\\t\\\\t\\\\}$/{c++; if\\(c==1\\) exit}')",
"Bash(dotnet test *)",
"Bash(git -C /Volumes/USBSSD/dev/Resgrid/Core log --oneline -1 -- Providers/Resgrid.Providers.Migrations/Migrations/M0094_AddIncidentCommandNameAndLocations.cs)"
]
},
"enableAllProjectMcpServers": true,
Expand Down Expand Up @@ -53,6 +58,17 @@
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/Users/shawn/.graperoot-pro/venv/bin/python3 \"/Users/shawn/.graperoot-pro/stop_hook.py\""
}
]
}
]
}
}
10 changes: 10 additions & 0 deletions Core/Resgrid.Config/ApiConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ public static class ApiConfig
/// </summary>
public const string CorsAllowedMethods = "GET,POST,PUT,DELETE,OPTIONS";

/// <summary>
/// Comma-separated list of additional origins allowed to make cross-origin (CORS) requests
/// to the API and eventing hubs, on top of the configured base urls, their subdomains and
/// their shared parent domain (see Resgrid.Config.CorsHelper). Entries with a scheme match
/// the exact origin ("http://localhost:8081"); bare hosts match that host on any scheme and
/// port ("dispatch.example.com"). A single "*" allows every origin — intended only for
/// isolated on-prem or development installs.
/// </summary>
public static string CorsAllowedOrigins = "";

/// <summary>
/// Key used for authing with the backend internal apis
/// </summary>
Expand Down
175 changes: 175 additions & 0 deletions Core/Resgrid.Config/CorsHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;

namespace Resgrid.Config
{
/// <summary>
/// Shared CORS origin validation used by the web front-ends (Services API and Eventing/SignalR).
/// An origin is allowed when it matches any of:
/// 1. An entry in <see cref="ApiConfig.CorsAllowedOrigins"/>. Entries with a scheme
/// ("http://localhost:8081") must match the origin's scheme, host and port exactly; bare
/// hosts ("dispatch.example.com") match that host on any scheme/port. A single "*" entry
/// allows every origin and is intended only for isolated on-prem or development installs.
/// 2. The host of one of the configured base urls (ResgridBaseUrl, ResgridApiBaseUrl,
/// ResgridEventingBaseUrl), or any subdomain of one of those hosts.
/// 3. The widest safe parent domain of a base-url host, or any subdomain of it. This is what
/// lets sibling apps call the API without being listed explicitly: with a base url of
/// qaapi.resgrid.dev the parent is resgrid.dev, so qadispatch.resgrid.dev is allowed.
/// Parent widening never crosses a public registry suffix (resgrid.co.uk will not widen
/// to co.uk) or a known shared-hosting suffix (myorg.github.io will not widen to
/// github.io) and is skipped entirely for IP addresses and single-label hosts.
/// </summary>
public static class CorsHelper
{
// Suffixes that must never be treated as a shared parent domain, because mutually
// untrusting parties register siblings directly under them. Two kinds live here:
//
// 1. Multi-part public registry suffixes (co.uk, com.au, ...). Widening api.resgrid.co.uk
// to co.uk would allow every site registered under that suffix to make credentialed
// calls. Single-part TLDs (com, dev, net, ...) need no listing: widening already stops
// at two labels, so a bare TLD can never be produced as a parent.
// 2. Private shared-hosting suffixes (github.io, azurewebsites.net, herokuapp.com, ...).
// A deployment served from myorg.github.io must not widen to github.io — every other
// tenant on the platform is an attacker-controlled sibling. Widening still works one
// level below the suffix (api.myorg.github.io widens to myorg.github.io).
//
// This is a curated snapshot of the common cases, not the full Public Suffix List. A
// deployment under a suffix not listed here should not rely on parent widening at all —
// list its sibling origins explicitly in ApiConfig.CorsAllowedOrigins instead.
private static readonly HashSet<string> _unsafeParentSuffixes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
// Public registry suffixes.
"co.uk", "org.uk", "me.uk", "ltd.uk", "plc.uk", "net.uk", "sch.uk", "ac.uk", "gov.uk", "nhs.uk",
"com.au", "net.au", "org.au", "edu.au", "gov.au", "id.au", "asn.au",
"co.nz", "net.nz", "org.nz", "govt.nz", "ac.nz",
"co.jp", "ne.jp", "or.jp", "go.jp", "ac.jp",
"com.br", "net.br", "org.br", "gov.br",
"com.mx", "org.mx", "gob.mx",
"co.za", "org.za", "gov.za", "web.za",
"co.in", "net.in", "org.in", "gen.in", "firm.in", "ind.in",
"com.cn", "net.cn", "org.cn", "gov.cn",
"com.sg", "com.hk", "com.tw", "com.my", "com.ph", "com.tr", "com.ar", "com.co",
"co.id", "co.kr", "co.th", "co.il",

// Private shared-hosting suffixes: code hosting pages.
"github.io", "gitlab.io", "bitbucket.io",

// Microsoft Azure.
"azurewebsites.net", "azurestaticapps.net", "azurecontainerapps.io", "cloudapp.net",
"cloudapp.azure.com", "trafficmanager.net", "azureedge.net", "azurefd.net",

// Amazon AWS (amazonaws.com blankets S3/ELB/execute-api regional hosts).
"amazonaws.com", "cloudfront.net", "elasticbeanstalk.com", "amplifyapp.com", "awsapprunner.com",

// Google Cloud / Firebase.
"appspot.com", "web.app", "firebaseapp.com", "run.app",

// Cloudflare.
"pages.dev", "workers.dev", "r2.dev", "trycloudflare.com",

// Other common PaaS / static hosting / tunnels.
"herokuapp.com", "netlify.app", "vercel.app", "now.sh", "surge.sh", "glitch.me",
"onrender.com", "fly.dev", "railway.app", "deno.dev", "koyeb.app",
"ondigitalocean.app", "digitaloceanspaces.com",
"repl.co", "replit.app",
"ngrok.io", "ngrok.app", "ngrok-free.app", "ngrok.dev", "loca.lt"
};

/// <summary>
/// Returns true when the supplied Origin header value is allowed to make cross-origin
/// requests. Suitable for use with CorsPolicyBuilder.SetIsOriginAllowed, including
/// policies that also call AllowCredentials (the matched origin is echoed back, never "*").
/// </summary>
public static bool IsAllowedOrigin(string origin)
{
if (String.IsNullOrWhiteSpace(origin) || !Uri.TryCreate(origin, UriKind.Absolute, out var originUri) || String.IsNullOrWhiteSpace(originUri.Host))
return false;

if (MatchesConfiguredOrigin(originUri))
return true;

foreach (var baseUrl in new[]
{
SystemBehaviorConfig.ResgridBaseUrl,
SystemBehaviorConfig.ResgridApiBaseUrl,
SystemBehaviorConfig.ResgridEventingBaseUrl
})
{
if (String.IsNullOrWhiteSpace(baseUrl) || !Uri.TryCreate(baseUrl, UriKind.Absolute, out var baseUri) || String.IsNullOrWhiteSpace(baseUri.Host))
continue;

if (HostMatchesOrIsSubdomainOf(originUri.Host, baseUri.Host))
return true;

var parentDomain = GetWidestSafeParentDomain(baseUri);
if (parentDomain != null && HostMatchesOrIsSubdomainOf(originUri.Host, parentDomain))
return true;
}

return false;
}

private static bool MatchesConfiguredOrigin(Uri originUri)
{
var configured = ApiConfig.CorsAllowedOrigins;
if (String.IsNullOrWhiteSpace(configured))
return false;

foreach (var rawEntry in configured.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
var entry = rawEntry.Trim();
if (entry.Length == 0)
continue;

if (entry == "*")
return true;

if (entry.Contains("://"))
{
if (Uri.TryCreate(entry, UriKind.Absolute, out var entryUri) &&
String.Equals(originUri.Scheme, entryUri.Scheme, StringComparison.OrdinalIgnoreCase) &&
String.Equals(originUri.Host, entryUri.Host, StringComparison.OrdinalIgnoreCase) &&
originUri.Port == entryUri.Port)
return true;
}
else if (String.Equals(originUri.Host, entry, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}

return false;
}

private static bool HostMatchesOrIsSubdomainOf(string originHost, string allowedHost)
{
return originHost.Equals(allowedHost, StringComparison.OrdinalIgnoreCase) ||
originHost.EndsWith("." + allowedHost, StringComparison.OrdinalIgnoreCase);
}

private static string GetWidestSafeParentDomain(Uri baseUri)
{
if (baseUri.HostNameType != UriHostNameType.Dns)
return null;

var labels = baseUri.Host.Split('.');

// Walk from the full host toward the apex (never past two labels), stopping before
// any public registry or shared-hosting suffix; the last safe candidate is the widest
// usable parent. qaapi.resgrid.dev -> resgrid.dev; api.resgrid.co.uk -> resgrid.co.uk
// (co.uk unsafe); api.myorg.github.io -> myorg.github.io (github.io unsafe);
// resgrid.com / localhost -> null (base-host matching already covers them).
string widest = null;
for (int start = 1; start <= labels.Length - 2; start++)
{
var candidate = String.Join(".", labels, start, labels.Length - start);
if (_unsafeParentSuffixes.Contains(candidate))
break;

widest = candidate;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return widest;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -325,4 +325,10 @@
<data name="PwdErrorNoLowercase" xml:space="preserve"><value>يجب أن تحتوي كلمة المرور على حرف صغير واحد على الأقل.</value></data>
<data name="PwdErrorTooShort" xml:space="preserve"><value>يجب أن تتكون كلمة المرور من {0} أحرف على الأقل.</value></data>
<data name="PwdMinLengthTooLow" xml:space="preserve"><value>لا يمكن أن يكون الحد الأدنى لطول كلمة المرور أقل من الإعداد الافتراضي للنظام وهو 8 أحرف.</value></data>
<data name="PermUseCalendarSyncLabel" xml:space="preserve"><value>Use Calendar Sync</value></data>
<data name="PermUseCalendarSyncNote" xml:space="preserve"><value>Controls who can activate and use calendar subscription URLs to sync Resgrid calendar events to external calendar applications.</value></data>
<data name="PermDispatchAppLoginLabel" xml:space="preserve"><value>Dispatch App Login</value></data>
<data name="PermDispatchAppLoginNote" xml:space="preserve"><value>Controls who can sign in to the Dispatch app. Dispatch shows private command, unit and responder communications for every incident, so restrict this if your members are not all dispatchers.</value></data>
<data name="PermCommandAppLoginLabel" xml:space="preserve"><value>Command App Login</value></data>
<data name="PermCommandAppLoginNote" xml:space="preserve"><value>Controls who can act as a commander: sign in to the IC app, establish incident command on a call, and view command boards. Narrowing this beyond Everyone also lets the people you pick help work any command board (assign and move resources, run timers and accountability) without holding an ICS position on it &#8212; useful for giving dispatchers a hand in the Dispatch app. While set to Everyone, board actions stay limited to the incident commander and assigned ICS roles.</value></data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -927,4 +927,10 @@
<data name="PwdErrorNoLowercase" xml:space="preserve"><value>Das Passwort muss mindestens einen Kleinbuchstaben enthalten.</value></data>
<data name="PwdErrorTooShort" xml:space="preserve"><value>Das Passwort muss mindestens {0} Zeichen lang sein.</value></data>
<data name="PwdMinLengthTooLow" xml:space="preserve"><value>Die Mindestlänge des Passworts darf nicht kleiner sein als der Systemstandard von 8 Zeichen.</value></data>
<data name="PermUseCalendarSyncLabel" xml:space="preserve"><value>Use Calendar Sync</value></data>
<data name="PermUseCalendarSyncNote" xml:space="preserve"><value>Controls who can activate and use calendar subscription URLs to sync Resgrid calendar events to external calendar applications.</value></data>
<data name="PermDispatchAppLoginLabel" xml:space="preserve"><value>Dispatch App Login</value></data>
<data name="PermDispatchAppLoginNote" xml:space="preserve"><value>Controls who can sign in to the Dispatch app. Dispatch shows private command, unit and responder communications for every incident, so restrict this if your members are not all dispatchers.</value></data>
<data name="PermCommandAppLoginLabel" xml:space="preserve"><value>Command App Login</value></data>
<data name="PermCommandAppLoginNote" xml:space="preserve"><value>Controls who can act as a commander: sign in to the IC app, establish incident command on a call, and view command boards. Narrowing this beyond Everyone also lets the people you pick help work any command board (assign and move resources, run timers and accountability) without holding an ICS position on it &#8212; useful for giving dispatchers a hand in the Dispatch app. While set to Everyone, board actions stay limited to the incident commander and assigned ICS roles.</value></data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,12 @@
<data name="PwdMinLengthTooLow" xml:space="preserve"><value>Minimum password length cannot be less than the system default of 8 characters.</value></data>
<data name="PermDeleteLogLabel" xml:space="preserve"><value>Delete Log Entries</value></data>
<data name="PermDeleteLogNote" xml:space="preserve"><value>Who in your department is allowed to delete log entries</value></data>
<data name="PermUseCalendarSyncLabel" xml:space="preserve"><value>Use Calendar Sync</value></data>
<data name="PermUseCalendarSyncNote" xml:space="preserve"><value>Controls who can activate and use calendar subscription URLs to sync Resgrid calendar events to external calendar applications.</value></data>
<data name="PermDispatchAppLoginLabel" xml:space="preserve"><value>Dispatch App Login</value></data>
<data name="PermDispatchAppLoginNote" xml:space="preserve"><value>Controls who can sign in to the Dispatch app. Dispatch shows private command, unit and responder communications for every incident, so restrict this if your members are not all dispatchers.</value></data>
<data name="PermCommandAppLoginLabel" xml:space="preserve"><value>Command App Login</value></data>
<data name="PermCommandAppLoginNote" xml:space="preserve"><value>Controls who can act as a commander: sign in to the IC app, establish incident command on a call, and view command boards. Narrowing this beyond Everyone also lets the people you pick help work any command board (assign and move resources, run timers and accountability) without holding an ICS position on it &#8212; useful for giving dispatchers a hand in the Dispatch app. While set to Everyone, board actions stay limited to the incident commander and assigned ICS roles.</value></data>
</root>


Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,12 @@
<data name="PwdErrorNoLowercase" xml:space="preserve"><value>La contraseña debe contener al menos una letra minúscula.</value></data>
<data name="PwdErrorTooShort" xml:space="preserve"><value>La contraseña debe tener al menos {0} caracteres.</value></data>
<data name="PwdMinLengthTooLow" xml:space="preserve"><value>La longitud mínima de la contraseña no puede ser menor que el valor predeterminado del sistema de 8 caracteres.</value></data>
<data name="PermUseCalendarSyncLabel" xml:space="preserve"><value>Use Calendar Sync</value></data>
<data name="PermUseCalendarSyncNote" xml:space="preserve"><value>Controls who can activate and use calendar subscription URLs to sync Resgrid calendar events to external calendar applications.</value></data>
<data name="PermDispatchAppLoginLabel" xml:space="preserve"><value>Dispatch App Login</value></data>
<data name="PermDispatchAppLoginNote" xml:space="preserve"><value>Controls who can sign in to the Dispatch app. Dispatch shows private command, unit and responder communications for every incident, so restrict this if your members are not all dispatchers.</value></data>
<data name="PermCommandAppLoginLabel" xml:space="preserve"><value>Command App Login</value></data>
<data name="PermCommandAppLoginNote" xml:space="preserve"><value>Controls who can act as a commander: sign in to the IC app, establish incident command on a call, and view command boards. Narrowing this beyond Everyone also lets the people you pick help work any command board (assign and move resources, run timers and accountability) without holding an ICS position on it &#8212; useful for giving dispatchers a hand in the Dispatch app. While set to Everyone, board actions stay limited to the incident commander and assigned ICS roles.</value></data>
</root>


Original file line number Diff line number Diff line change
Expand Up @@ -927,4 +927,10 @@
<data name="PwdErrorNoLowercase" xml:space="preserve"><value>Le mot de passe doit contenir au moins une lettre minuscule.</value></data>
<data name="PwdErrorTooShort" xml:space="preserve"><value>Le mot de passe doit comporter au moins {0} caractères.</value></data>
<data name="PwdMinLengthTooLow" xml:space="preserve"><value>La longueur minimale du mot de passe ne peut pas être inférieure à la valeur par défaut du système de 8 caractères.</value></data>
<data name="PermUseCalendarSyncLabel" xml:space="preserve"><value>Use Calendar Sync</value></data>
<data name="PermUseCalendarSyncNote" xml:space="preserve"><value>Controls who can activate and use calendar subscription URLs to sync Resgrid calendar events to external calendar applications.</value></data>
<data name="PermDispatchAppLoginLabel" xml:space="preserve"><value>Dispatch App Login</value></data>
<data name="PermDispatchAppLoginNote" xml:space="preserve"><value>Controls who can sign in to the Dispatch app. Dispatch shows private command, unit and responder communications for every incident, so restrict this if your members are not all dispatchers.</value></data>
<data name="PermCommandAppLoginLabel" xml:space="preserve"><value>Command App Login</value></data>
<data name="PermCommandAppLoginNote" xml:space="preserve"><value>Controls who can act as a commander: sign in to the IC app, establish incident command on a call, and view command boards. Narrowing this beyond Everyone also lets the people you pick help work any command board (assign and move resources, run timers and accountability) without holding an ICS position on it &#8212; useful for giving dispatchers a hand in the Dispatch app. While set to Everyone, board actions stay limited to the incident commander and assigned ICS roles.</value></data>
</root>
Loading
Loading