mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 18:08:20 +08:00
feat(ql3): fence console capabilities by session
This commit is contained in:
@@ -24,7 +24,7 @@ const ASSETS = Object.freeze([
|
||||
name: 'index.html',
|
||||
field: 'html',
|
||||
maximumBytes: 32 * 1024,
|
||||
digest: '429d7b3dd2da4989865be6ac07180cc9c3ebdcbbac5028054cddaac870ad520c',
|
||||
digest: '7e024103aaf03553995b09bfb9d6da5ba058ce77f7fff65c56b564017c79f28a',
|
||||
}),
|
||||
Object.freeze({
|
||||
name: 'app.css',
|
||||
@@ -42,7 +42,7 @@ const ASSETS = Object.freeze([
|
||||
name: 'app.js',
|
||||
field: 'javascript',
|
||||
maximumBytes: 32 * 1024,
|
||||
digest: '365ccd43ae2aa4b11a0ab3d142cd04e89ec0b96711bef253f63584e8182589ee',
|
||||
digest: '224895776a1df1b4571a8cdb9ce8126861d0284e1fef801e6de3ef31ca96c931',
|
||||
}),
|
||||
] as const);
|
||||
|
||||
|
||||
@@ -577,6 +577,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
const server = await startClusterCopilotConsoleServer({
|
||||
allowedOperations: operations,
|
||||
assets,
|
||||
executor: Object.freeze({
|
||||
execute(request: Readonly<ClusterCopilotConsoleReadRequest>) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '../management-support/pluginPackageManagementClient';
|
||||
import { type ClusterCopilotConsoleAssets } from './assets';
|
||||
import {
|
||||
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
|
||||
CLUSTER_COPILOT_CONSOLE_READ_RESPONSE_SCHEMA,
|
||||
InvalidClusterCopilotConsoleReadRequestError,
|
||||
normalizeClusterCopilotConsoleReadRequest,
|
||||
@@ -43,6 +44,7 @@ export interface ClusterCopilotConsoleExecutor {
|
||||
}
|
||||
|
||||
export interface ClusterCopilotConsoleServerOptions {
|
||||
readonly allowedOperations: readonly ClusterCopilotConsoleReadOperation[];
|
||||
readonly assets: Readonly<ClusterCopilotConsoleAssets>;
|
||||
readonly executor: ClusterCopilotConsoleExecutor;
|
||||
readonly networkBoundary?: ClusterCopilotConsoleNetworkBoundary;
|
||||
@@ -69,6 +71,31 @@ export class ClusterCopilotConsoleConfigurationError extends TypeError {
|
||||
}
|
||||
|
||||
const SESSION_TOKEN = /^[A-Za-z0-9_-]{43}$/;
|
||||
const CAPABILITIES_PATH = '/api/v1/session/capabilities';
|
||||
const CAPABILITIES_REQUEST_SCHEMA =
|
||||
'qinglong/cluster-copilot-console-capabilities-request@v1';
|
||||
const CAPABILITIES_SCHEMA = 'qinglong/cluster-copilot-console-capabilities@v1';
|
||||
const RUN_MANAGEMENT_OPERATIONS = Object.freeze([
|
||||
'run_cancellation_status',
|
||||
'run_cancellation_blocked_list',
|
||||
'run_cancellation_inspect',
|
||||
] as const satisfies readonly ClusterCopilotConsoleReadOperation[]);
|
||||
const WORKER_MANAGEMENT_OPERATIONS = Object.freeze([
|
||||
'worker_list',
|
||||
'worker_inspect',
|
||||
] as const satisfies readonly ClusterCopilotConsoleReadOperation[]);
|
||||
const PACKAGE_MANAGEMENT_OPERATIONS = Object.freeze([
|
||||
'package_list',
|
||||
'package_inspect',
|
||||
] as const satisfies readonly ClusterCopilotConsoleReadOperation[]);
|
||||
const OPTIONAL_OPERATION_GROUPS = Object.freeze([
|
||||
RUN_MANAGEMENT_OPERATIONS,
|
||||
WORKER_MANAGEMENT_OPERATIONS,
|
||||
PACKAGE_MANAGEMENT_OPERATIONS,
|
||||
]);
|
||||
const OPTIONAL_OPERATIONS = new Set<ClusterCopilotConsoleReadOperation>(
|
||||
OPTIONAL_OPERATION_GROUPS.flat(),
|
||||
);
|
||||
const SESSION_DIGEST_DOMAIN = Buffer.from(
|
||||
'qinglong-cluster-copilot-console-session-v1\0',
|
||||
'utf8',
|
||||
@@ -219,6 +246,57 @@ function targetPath(
|
||||
return request.url === undefined ? null : READ_ROUTES[request.url] ?? null;
|
||||
}
|
||||
|
||||
function validatedAllowedOperations(
|
||||
value: unknown,
|
||||
): readonly ClusterCopilotConsoleReadOperation[] {
|
||||
if (!Array.isArray(value)) return invalid();
|
||||
const canonical = new Set<ClusterCopilotConsoleReadOperation>(
|
||||
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS,
|
||||
);
|
||||
const operations = value as ClusterCopilotConsoleReadOperation[];
|
||||
const selected = new Set(operations);
|
||||
if (
|
||||
selected.size !== operations.length ||
|
||||
operations.some((operation) => !canonical.has(operation)) ||
|
||||
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS.some(
|
||||
(operation) =>
|
||||
!OPTIONAL_OPERATIONS.has(operation) && !selected.has(operation),
|
||||
) ||
|
||||
OPTIONAL_OPERATION_GROUPS.some((group) => {
|
||||
const selectedCount = group.filter((operation) =>
|
||||
selected.has(operation),
|
||||
).length;
|
||||
return selectedCount !== 0 && selectedCount !== group.length;
|
||||
})
|
||||
) {
|
||||
return invalid();
|
||||
}
|
||||
return Object.freeze(
|
||||
CLUSTER_COPILOT_CONSOLE_READ_OPERATIONS.filter((operation) =>
|
||||
selected.has(operation),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function operationGroupEnabled(
|
||||
allowedOperations: ReadonlySet<ClusterCopilotConsoleReadOperation>,
|
||||
operations: readonly ClusterCopilotConsoleReadOperation[],
|
||||
): boolean {
|
||||
return operations.every((operation) => allowedOperations.has(operation));
|
||||
}
|
||||
|
||||
function validateCapabilitiesRequest(value: unknown): void {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
Array.isArray(value) ||
|
||||
Object.keys(value).length !== 1 ||
|
||||
(value as Record<string, unknown>).schema !== CAPABILITIES_REQUEST_SCHEMA
|
||||
) {
|
||||
throw new InvalidClusterCopilotConsoleReadRequestError();
|
||||
}
|
||||
}
|
||||
|
||||
function authorize(
|
||||
request: IncomingMessage,
|
||||
expectedOrigin: string,
|
||||
@@ -331,7 +409,13 @@ function remoteFailure(
|
||||
export async function startClusterCopilotConsoleServer(
|
||||
options: ClusterCopilotConsoleServerOptions,
|
||||
): Promise<Readonly<ClusterCopilotConsoleServer>> {
|
||||
const optionKeys = ['assets', 'executor', 'port', 'sessionDigest'];
|
||||
const optionKeys = [
|
||||
'allowedOperations',
|
||||
'assets',
|
||||
'executor',
|
||||
'port',
|
||||
'sessionDigest',
|
||||
];
|
||||
if (Object.hasOwn(options, 'networkBoundary')) {
|
||||
optionKeys.push('networkBoundary');
|
||||
}
|
||||
@@ -342,6 +426,8 @@ export async function startClusterCopilotConsoleServer(
|
||||
'html',
|
||||
'javascript',
|
||||
]);
|
||||
const operations = validatedAllowedOperations(record.allowedOperations);
|
||||
const allowedOperations = new Set(operations);
|
||||
const networkBoundary =
|
||||
record.networkBoundary === undefined
|
||||
? 'host-loopback'
|
||||
@@ -412,16 +498,23 @@ export async function startClusterCopilotConsoleServer(
|
||||
}
|
||||
}
|
||||
|
||||
const capabilitiesRequest =
|
||||
request.method === 'POST' && request.url === CAPABILITIES_PATH;
|
||||
const operation = targetPath(request);
|
||||
if (
|
||||
!hostMatches ||
|
||||
operation === null ||
|
||||
(!capabilitiesRequest && operation === null) ||
|
||||
!authorize(request, expectedOrigin, sessionDigest)
|
||||
) {
|
||||
sendJson(response, 404, Object.freeze({ code: 'not_found' }));
|
||||
request.resume();
|
||||
return;
|
||||
}
|
||||
if (!capabilitiesRequest && !allowedOperations.has(operation!)) {
|
||||
sendJson(response, 404, Object.freeze({ code: 'not_found' }));
|
||||
request.resume();
|
||||
return;
|
||||
}
|
||||
if (inFlight >= CLUSTER_COPILOT_CONSOLE_LIMITS.maximumConcurrentRequests) {
|
||||
sendJson(
|
||||
response,
|
||||
@@ -439,6 +532,41 @@ export async function startClusterCopilotConsoleServer(
|
||||
inFlight += 1;
|
||||
try {
|
||||
const body = await readJsonBody(request);
|
||||
if (capabilitiesRequest) {
|
||||
validateCapabilitiesRequest(body);
|
||||
sendJson(
|
||||
response,
|
||||
200,
|
||||
Object.freeze({
|
||||
schema: CAPABILITIES_SCHEMA,
|
||||
operations,
|
||||
authorities: Object.freeze({
|
||||
projectObservation: 'server_only',
|
||||
runManagement: operationGroupEnabled(
|
||||
allowedOperations,
|
||||
RUN_MANAGEMENT_OPERATIONS,
|
||||
)
|
||||
? 'server_only'
|
||||
: 'disabled',
|
||||
workerManagement: operationGroupEnabled(
|
||||
allowedOperations,
|
||||
WORKER_MANAGEMENT_OPERATIONS,
|
||||
)
|
||||
? 'server_only'
|
||||
: 'disabled',
|
||||
packageManagement: operationGroupEnabled(
|
||||
allowedOperations,
|
||||
PACKAGE_MANAGEMENT_OPERATIONS,
|
||||
)
|
||||
? 'server_only'
|
||||
: 'disabled',
|
||||
}),
|
||||
mutation: false,
|
||||
upstreamReads: 0,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeClusterCopilotConsoleReadRequest(body);
|
||||
if (normalized.operation !== operation) {
|
||||
throw new InvalidClusterCopilotConsoleReadRequestError();
|
||||
|
||||
Reference in New Issue
Block a user