@gen3/workspaces 0.12.72 → 0.12.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dts/api/workspaceAssetsApi.d.ts.map +1 -1
- package/dist/dts/api/workspaceAssetsApi.js +7 -2
- package/dist/dts/api/workspaceAssetsApi.js.map +1 -1
- package/dist/dts/server/workspaceAssetsHandler.d.ts.map +1 -1
- package/dist/dts/server/workspaceAssetsHandler.js +33 -20
- package/dist/dts/server/workspaceAssetsHandler.js.map +1 -1
- package/dist/server/cjs/server.js +38 -21
- package/dist/server/cjs/server.js.map +1 -1
- package/dist/server/esm/server.js +38 -21
- package/dist/server/esm/server.js.map +1 -1
- package/package.json +4 -4
|
@@ -334,20 +334,24 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
|
|
|
334
334
|
// Inject identity headers so workspace-proxy auth passes.
|
|
335
335
|
// Direct server-side calls bypass nginx (which normally injects X-Gen3-User-ID /
|
|
336
336
|
// REMOTE_USER from the validated access_token), so we must add them explicitly.
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
337
|
+
// SECURITY: decodeJwtClaims below does not verify the JWT signature — it relies on
|
|
338
|
+
// Ambassador/revproxy having already done so before this code path is reached. When
|
|
339
|
+
// serverExtensionProxyBaseUrl (WORKSPACE_PROXY_URL) targets workspace-proxy directly,
|
|
340
|
+
// that verification is skipped entirely, so a caller could supply a correctly-shaped
|
|
341
|
+
// but unsigned/forged JWT to impersonate another user via REMOTE_USER/X-Gen3-User-ID.
|
|
342
|
+
// Only point serverExtensionProxyBaseUrl at a network path a user's own request cannot
|
|
343
|
+
// otherwise forge, or add real signature verification (e.g. against Fence's JWKS)
|
|
344
|
+
// here before trusting these claims.
|
|
345
|
+
const jwt = getToken ? getToken(req) : extractTokenFallback(req);
|
|
346
|
+
if (jwt) {
|
|
347
|
+
const claims = decodeJwtClaims(jwt);
|
|
348
|
+
const rawUsername = claims?.sub ?? claims?.preferred_username ?? claims?.username;
|
|
349
|
+
if (typeof rawUsername === 'string') {
|
|
350
|
+
const safeUsername = rawUsername.replace(/[\r\n]/g, '');
|
|
351
|
+
const safeJwt = jwt.replace(/[\r\n]/g, '');
|
|
352
|
+
forwardHeaders['Authorization'] = `Bearer ${safeJwt}`;
|
|
353
|
+
forwardHeaders['REMOTE_USER'] = safeUsername;
|
|
354
|
+
forwardHeaders['X-Gen3-User-ID'] = safeUsername;
|
|
351
355
|
}
|
|
352
356
|
}
|
|
353
357
|
const method = (req.method || 'GET').toUpperCase();
|
|
@@ -358,7 +362,9 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
|
|
|
358
362
|
].includes(method)) {
|
|
359
363
|
try {
|
|
360
364
|
body = await readBody$2(req);
|
|
361
|
-
} catch
|
|
365
|
+
} catch (error) {
|
|
366
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
367
|
+
console.error(`[workspace-assets] failed to read request body for ${method} ${targetUrl}: ${message}`);
|
|
362
368
|
res.status(500).end('Failed to read request body');
|
|
363
369
|
return;
|
|
364
370
|
}
|
|
@@ -366,7 +372,8 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
|
|
|
366
372
|
forwardHeaders['content-length'] = String(body.length);
|
|
367
373
|
}
|
|
368
374
|
}
|
|
369
|
-
|
|
375
|
+
const startedAt = Date.now();
|
|
376
|
+
console.warn(`[workspace-assets] proxying ${method} ${targetUrl}${body?.length ? ` (body: ${body.length} bytes)` : ''}`);
|
|
370
377
|
await new Promise((resolve)=>{
|
|
371
378
|
const parsed = new URL(targetUrl);
|
|
372
379
|
const transport = parsed.protocol === 'https:' ? https : http;
|
|
@@ -378,7 +385,8 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
|
|
|
378
385
|
headers: forwardHeaders
|
|
379
386
|
};
|
|
380
387
|
const proxyReq = transport.request(reqOptions, (proxyRes)=>{
|
|
381
|
-
|
|
388
|
+
const statusCode = proxyRes.statusCode ?? 502;
|
|
389
|
+
res.status(statusCode);
|
|
382
390
|
const skip = new Set([
|
|
383
391
|
'transfer-encoding',
|
|
384
392
|
'content-length'
|
|
@@ -388,18 +396,24 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
|
|
|
388
396
|
res.setHeader(key, value);
|
|
389
397
|
}
|
|
390
398
|
}
|
|
399
|
+
const log = statusCode >= 400 ? console.error : console.warn;
|
|
400
|
+
log(`[workspace-assets] ${method} ${targetUrl} -> ${statusCode} (${Date.now() - startedAt}ms)`);
|
|
391
401
|
proxyRes.pipe(res, {
|
|
392
402
|
end: true
|
|
393
403
|
});
|
|
394
404
|
proxyRes.on('end', resolve);
|
|
395
405
|
});
|
|
396
406
|
proxyReq.on('error', (err)=>{
|
|
397
|
-
console.error(
|
|
407
|
+
console.error(`[workspace-assets] extension proxy failed for ${method} ${targetUrl} after ${Date.now() - startedAt}ms: ${err.code ?? 'UNKNOWN'} ${err.message}`);
|
|
398
408
|
if (!res.headersSent) {
|
|
399
409
|
res.status(502).end('Extension proxy unreachable');
|
|
400
410
|
}
|
|
401
411
|
resolve();
|
|
402
412
|
});
|
|
413
|
+
proxyReq.on('timeout', ()=>{
|
|
414
|
+
console.error(`[workspace-assets] extension proxy timed out for ${method} ${targetUrl} after ${Date.now() - startedAt}ms`);
|
|
415
|
+
proxyReq.destroy();
|
|
416
|
+
});
|
|
403
417
|
if (body?.length) {
|
|
404
418
|
proxyReq.write(body);
|
|
405
419
|
}
|
|
@@ -494,7 +508,6 @@ function createWorkspaceAssetsHandler(options) {
|
|
|
494
508
|
return `${proto}://${host}`;
|
|
495
509
|
})();
|
|
496
510
|
const targetUrl = `${proxyBase}${serverExtensionProxyPath}/${extensionPath}${qs}`;
|
|
497
|
-
console.log('[workspace-assets] proxying extension API to', targetUrl);
|
|
498
511
|
return proxyServerExtension(req, res, targetUrl, getToken);
|
|
499
512
|
}
|
|
500
513
|
const allowedFiles = allowedFilesByTier[tier];
|
|
@@ -1185,8 +1198,12 @@ const DEFAULT_OPTIONS = {
|
|
|
1185
1198
|
// Optional internal k8s service base URL for the extension proxy. When set,
|
|
1186
1199
|
// proxy requests target this address directly instead of routing through the
|
|
1187
1200
|
// ingress (avoiding self-signed-cert issues). Readable from the JSON config
|
|
1188
|
-
// file or the
|
|
1189
|
-
serverExtensionProxyBaseUrl:
|
|
1201
|
+
// file or the WORKSPACE_PROXY_URL env var.
|
|
1202
|
+
serverExtensionProxyBaseUrl: process.env.WORKSPACE_PROXY_URL || undefined,
|
|
1203
|
+
// Direct calls to serverExtensionProxyBaseUrl bypass nginx, which normally
|
|
1204
|
+
// injects the X-Gen3-User-ID/REMOTE_USER headers workspace-proxy requires —
|
|
1205
|
+
// reuse this app's cookie extraction to inject them explicitly instead.
|
|
1206
|
+
getToken: (req)=>getAccessToken(req.headers['cookie'] ?? '') ?? null,
|
|
1190
1207
|
// in-browser Pyodide kernel must not be offered alongside it.
|
|
1191
1208
|
additionalDisabledExtensions: [
|
|
1192
1209
|
'@jupyterlite/pyodide-kernel-extension:kernel'
|