mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 00:19:42 +02:00
fix(multiuser): close cross-user web-layer scoping holes found in review
The opt-in multi-user feature's only enforcement is web-layer scoping (all sessions share one OS account). An adversarial review found 8 critical + 7 high cross-user holes that defeated it, plus mediums; all fixed here. Single-user (flag-off) behavior stays byte-identical apart from documented consistency deltas. Ownership / confinement: - DELETE /api/sessions (bulk) + /:id now owner-scope / findSessionOrFail - quick-start, cron (create+fire), scheduled runs confine workingDir to the owner's space; case link/docker-link/docker-import confine the host path - resolveCasePath no longer resolves linked cases for non-admins; foreign remote/docker cases are skipped (fall through to the caller's own local case) - history, subagents/workflows, mux-sessions, orchestrator, cron run-history, away-digest, and remote/docker host reads are owner- or admin-scoped Permission policy (section 6.3): - non-granted users are downgraded at every spawn site incl. legacy /api/scheduled, PlanOrchestrator one-shots, remote launch, and the cron-fire gemini/codex bypass switches; resolveClaudeModeForUsername now fails closed Auth / store: - verify-first login throttle (a correct password is never locked out), /ws terminal subject to the change-password lockbox, cookie fast-path re-validates identity live, role/grant changes revoke sessions, admin delete runs the last-admin guard before any teardown - users.json: distinguish missing (ENOENT) from corrupt/unreadable so a bad read can't overwrite all accounts; unique per-process temp write path Event streams: - debounced session:updated + batched task:updated, clipboard, and push notifications route by owner (fail closed); getLightState hides machine-wide globalStats from non-admins Tests: two suites updated to assert the fixed (secure) behavior. tsc, eslint, and test:ci all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ import type { TerminalMultiplexer } from './mux-interface.js';
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { RESEARCH_AGENT_PROMPT, PLANNER_PROMPT } from './prompts/index.js';
|
||||
import { getErrorMessage, type PlanItem } from './types.js';
|
||||
import { getErrorMessage, type PlanItem, type ClaudeMode } from './types.js';
|
||||
|
||||
// Re-export for backward compatibility
|
||||
export type { PlanItem };
|
||||
@@ -130,18 +130,28 @@ export class PlanOrchestrator {
|
||||
private taskDescription = '';
|
||||
private researchModel: string;
|
||||
private plannerModel: string;
|
||||
// Multi-user permission threading: the resolved claudeMode/owner/allowedTools for the
|
||||
// internal research/planner one-shots. Left undefined = today's single-user behavior
|
||||
// (the caller threads the resolved global mode, byte-identical when !isMultiUserMode()).
|
||||
private claudeMode?: ClaudeMode;
|
||||
private owner?: string;
|
||||
private allowedTools?: string;
|
||||
|
||||
constructor(
|
||||
mux: TerminalMultiplexer,
|
||||
workingDir: string = process.cwd(),
|
||||
outputDir?: string,
|
||||
modelConfig?: { defaultModel?: string; agentTypeOverrides?: Record<string, string> }
|
||||
modelConfig?: { defaultModel?: string; agentTypeOverrides?: Record<string, string> },
|
||||
security?: { claudeMode?: ClaudeMode; owner?: string; allowedTools?: string }
|
||||
) {
|
||||
this.mux = mux;
|
||||
this.workingDir = workingDir;
|
||||
this.outputDir = outputDir;
|
||||
this.researchModel = modelConfig?.agentTypeOverrides?.explore || modelConfig?.defaultModel || DEFAULT_MODEL;
|
||||
this.plannerModel = modelConfig?.agentTypeOverrides?.review || modelConfig?.defaultModel || DEFAULT_MODEL;
|
||||
this.claudeMode = security?.claudeMode;
|
||||
this.owner = security?.owner;
|
||||
this.allowedTools = security?.allowedTools;
|
||||
}
|
||||
|
||||
private saveAgentOutput(agentType: string, prompt: string, result: unknown, durationMs: number): void {
|
||||
@@ -424,6 +434,12 @@ export class PlanOrchestrator {
|
||||
mux: this.mux,
|
||||
useMux: false,
|
||||
mode: 'claude',
|
||||
// Section 6.3: run this one-shot under the caller-resolved permission mode/owner so a
|
||||
// non-granted multi-user user cannot regain --dangerously-skip-permissions. Undefined
|
||||
// (single-user, not threaded) is byte-identical to today (Session keeps its default).
|
||||
claudeMode: this.claudeMode,
|
||||
allowedTools: this.allowedTools,
|
||||
owner: this.owner,
|
||||
});
|
||||
|
||||
this.runningSessions.add(session);
|
||||
@@ -580,6 +596,10 @@ export class PlanOrchestrator {
|
||||
mux: this.mux,
|
||||
useMux: false,
|
||||
mode: 'claude',
|
||||
// Section 6.3: same permission-mode/owner threading as the research one-shot above.
|
||||
claudeMode: this.claudeMode,
|
||||
allowedTools: this.allowedTools,
|
||||
owner: this.owner,
|
||||
});
|
||||
|
||||
this.runningSessions.add(session);
|
||||
|
||||
Reference in New Issue
Block a user