apply the containers:exec permission check to the websocket terminal path

This commit is contained in:
jarek committed 2026-08-20 08:28:26 +02:00
1 parent f50c3e52f3
commit f8bfd756ef
4 files changed
+81 -1

No files matched your search

+17
View File
@@ -267,6 +267,23 @@ async function handleTerminalConnection(ws, url, connId) {
}
}
// Opening a shell requires the containers:exec permission, same as the REST exec endpoint.
if (ws.__auth && typeof globalThis.__canExecForUser === 'function') {
try {
const allowed = await globalThis.__canExecForUser(ws.__auth, envId);
if (!allowed) {
console.warn(`[WS] exec denied: user=${ws.__auth.username} envId=${envId}`);
ws.send(JSON.stringify({ type: 'error', message: 'Permission denied' }));
ws.close(1008, 'exec permission denied');
return;
}
} catch (err) {
console.error('[WS] exec permission check failed:', err);
ws.close(1011, 'internal error');
return;
}
}
try {
// Resolve Docker target via SvelteKit app's database
let target;
+22 -1
View File
@@ -1,8 +1,9 @@
import { validateSessionById, isAuthEnabled, SESSION_COOKIE } from './auth';
import { validateSessionById, isAuthEnabled, SESSION_COOKIE, getUserPermissionsForEnvironment } from './auth';
import { validateApiToken } from './api-tokens';
import { isEnterprise } from './license';
import { userHasAdminRole, userCanAccessEnvironment } from './db';
import { parseCookieHeader } from '$lib/utils/cookie-parse';
import { canExecDecision } from './ws-exec-core';
export interface WsUpgradeAuth {
userId: number;
@@ -68,6 +69,21 @@ export async function canAccessEnvForUser(
return userCanAccessEnvironment(auth.userId, environmentId);
}
/**
* Whether the WS user may open a container exec/terminal session. Mirrors the
* REST gate `auth.can('containers','exec',envId)`. Pure logic lives in
* ws-exec-core; this binds the real enterprise + permission deps.
*/
export async function canExecForUser(
auth: WsUpgradeAuth,
environmentId: number | undefined | null
): Promise<boolean> {
return canExecDecision(auth, environmentId, {
isEnterprise,
getPerms: getUserPermissionsForEnvironment
});
}
declare global {
// eslint-disable-next-line no-var
var __authenticateWsUpgrade:
@@ -77,7 +93,12 @@ declare global {
var __canAccessEnvForUser:
| ((auth: WsUpgradeAuth, envId: number | undefined | null) => Promise<boolean>)
| undefined;
// eslint-disable-next-line no-var
var __canExecForUser:
| ((auth: WsUpgradeAuth, envId: number | undefined | null) => Promise<boolean>)
| undefined;
}
globalThis.__authenticateWsUpgrade = authenticateWsUpgrade;
globalThis.__canAccessEnvForUser = canAccessEnvForUser;
globalThis.__canExecForUser = canExecForUser;
+30
View File
@@ -0,0 +1,30 @@
/**
* Pure decision core for the WS exec gate. No imports, so it is unit-testable in
* the bun runner without dragging in better-sqlite3 via auth/db. Deps are
* injected; ws-auth.ts binds the real ones.
*
* Mirrors the REST gate `auth.can('containers','exec',envId)`: free edition /
* auth-disabled / admin all pass, exactly as checkPermission() does; enterprise
* requires the env-scoped `containers:exec` permission.
*/
export interface ExecAuth {
userId: number;
isAdmin: boolean;
authDisabled: boolean;
}
export async function canExecDecision(
auth: ExecAuth,
environmentId: number | undefined | null,
deps: {
isEnterprise: () => Promise<boolean>;
getPerms: (userId: number, envId: number) => Promise<{ containers?: string[] }>;
}
): Promise<boolean> {
if (auth.authDisabled) return true;
if (auth.isAdmin) return true;
if (!(await deps.isEnterprise())) return true;
if (environmentId == null) return false;
const perms = await deps.getPerms(auth.userId, environmentId);
return perms.containers?.includes('exec') ?? false;
}
+12
View File
@@ -619,6 +619,18 @@ function webSocketPlugin(): Plugin {
}
}
// Opening a shell requires the containers:exec permission, same as the REST exec endpoint.
const canExecFn = (globalThis as any).__canExecForUser;
if (typeof canExecFn === 'function') {
const allowed = await canExecFn(wsAuth, envId);
if (!allowed) {
console.warn(`[WS] exec denied: user=${wsAuth.username} envId=${envId}`);
ws.send(JSON.stringify({ type: 'error', message: 'Permission denied' }));
ws.close(1008, 'exec permission denied');
return;
}
}
const target = getDockerTarget(envId);
try {