apply the containers:exec permission check to the websocket terminal path
This commit is contained in:
4 files changed
+81
-1
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user