mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-06 08:58:05 +00:00
69edb0dcbb
* 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.
36 lines
1.5 KiB
TypeScript
36 lines
1.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import path from 'path';
|
|
import { isManagedByComposeDir } from '../utils/managed-containers';
|
|
|
|
const COMPOSE_DIR = path.resolve('/srv/compose');
|
|
|
|
function withWorkingDir(workingDir?: string): { Labels?: Record<string, string> } {
|
|
return workingDir === undefined
|
|
? {}
|
|
: { Labels: { 'com.docker.compose.project.working_dir': workingDir } };
|
|
}
|
|
|
|
describe('isManagedByComposeDir', () => {
|
|
it('treats a container in a subdirectory of COMPOSE_DIR as managed', () => {
|
|
expect(isManagedByComposeDir(withWorkingDir(path.join(COMPOSE_DIR, 'web')), COMPOSE_DIR)).toBe(true);
|
|
});
|
|
|
|
it('treats a container launched from the COMPOSE_DIR root as managed', () => {
|
|
expect(isManagedByComposeDir(withWorkingDir(COMPOSE_DIR), COMPOSE_DIR)).toBe(true);
|
|
});
|
|
|
|
it('treats a container outside COMPOSE_DIR as unmanaged', () => {
|
|
expect(isManagedByComposeDir(withWorkingDir('/opt/other/stack'), COMPOSE_DIR)).toBe(false);
|
|
});
|
|
|
|
it('does not match a sibling directory that shares the COMPOSE_DIR prefix', () => {
|
|
// /srv/compose-extra must not be considered inside /srv/compose.
|
|
expect(isManagedByComposeDir(withWorkingDir(`${COMPOSE_DIR}-extra/web`), COMPOSE_DIR)).toBe(false);
|
|
});
|
|
|
|
it('treats a container with no compose working-dir label as unmanaged', () => {
|
|
expect(isManagedByComposeDir(withWorkingDir(undefined), COMPOSE_DIR)).toBe(false);
|
|
expect(isManagedByComposeDir({ Labels: {} }, COMPOSE_DIR)).toBe(false);
|
|
});
|
|
});
|