mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 11:47:11 +00:00
fix(observability): gate global logs to admins, scope to managed containers, harden SSE (#1254)
* fix(observability): gate global logs to admins, scope to managed containers, harden SSE Make the Logs feed an administrator view enforced on both sides (requireAdmin on the /api/logs/global poll and SSE routes; the Logs nav item plus a redirect guard on the frontend), and scope the feed to Sencho-managed containers only via a shared isManagedByComposeDir helper that /stats now reuses. Harden the SSE stream: a stateful frame demuxer that survives chunk boundaries so a Docker frame split across reads is reassembled instead of dropped or garbled; a per-stream error listener so one broken follow stream cannot crash the event loop (it posts a single degraded notice and keeps the others alive); a cap on concurrent follow streams with a truncation notice; a bounded initial tail; and backpressure that pauses the source streams when the client is slow and resumes on drain. Bound the polling snapshot's per-container fan-out with a concurrency limit. Add process-local, in-memory log-stream counters exposed at the admin-only /api/system/log-stream-metrics endpoint (active connections, lines streamed, attach and frame errors). Collapse the view to the local hub and remove the dead remote-node handling. * fix(observability): close remote-proxy bypass of the global-logs admin gate The logs feed's requireAdmin lives in the local route handler, which the remote proxy skips when forwarding a request whose nodeId targets a remote node. A hub user could therefore request /api/logs/global*, /api/logs/global/stream, or /api/system/log-stream-metrics with x-node-id (or ?nodeId= for the SSE transport) pointing at a remote node and have it served as the node-proxy admin on the far side, sidestepping the gate entirely. Add these paths to HUB_ONLY_PREFIXES so hubOnlyGuard rejects a remote nodeId with 403 before the proxy runs, matching the existing protection on audit-log, scheduled-tasks, and notification-routes. Add regression tests covering the collection path, the SSE sub-path (both the x-node-id header and the ?nodeId= query transport), and the stream-metrics endpoint.
This commit is contained in:
@@ -23,11 +23,14 @@ export function isProxyExemptPath(path: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Path prefixes that are hub-only: they manage state owned by the local hub
|
||||
// (centralized audit, fleet schedules, notification routing rules). Routed
|
||||
// to the local hub when nodeId resolves to local, but rejected with 409 when
|
||||
// nodeId resolves to a remote node so a script/curl call cannot trick the
|
||||
// proxy into forwarding hub-only authority across a node boundary.
|
||||
// Path prefixes that are hub-only: they manage or expose state owned by the
|
||||
// local hub (centralized audit, fleet schedules, notification routing rules,
|
||||
// the admin-only aggregated logs feed and its stream counters). Routed to the
|
||||
// local hub when nodeId resolves to local, but rejected when nodeId resolves
|
||||
// to a remote node so a script/curl call cannot trick the proxy into
|
||||
// forwarding hub-only authority across a node boundary. This matters for the
|
||||
// logs feed in particular: its admin gate lives in the local route handler,
|
||||
// which the proxy would skip entirely when forwarding a remote nodeId.
|
||||
//
|
||||
// Entries are stored with a trailing slash; the matcher accepts the exact
|
||||
// collection path (without the trailing slash) AND any sub-path under it,
|
||||
@@ -44,6 +47,8 @@ export const HUB_ONLY_PREFIXES: readonly string[] = [
|
||||
'/api/scheduled-tasks/',
|
||||
'/api/audit-log/',
|
||||
'/api/notification-routes/',
|
||||
'/api/logs/global/',
|
||||
'/api/system/log-stream-metrics/',
|
||||
];
|
||||
|
||||
/** Returns true when the path is hub-only and must not be proxied to a remote node. */
|
||||
|
||||
Reference in New Issue
Block a user