@gen3/workspaces 0.12.72 → 0.12.74
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 +71 -20
- package/dist/dts/server/workspaceAssetsHandler.js.map +1 -1
- package/dist/server/cjs/server.js +62 -22
- package/dist/server/cjs/server.js.map +1 -1
- package/dist/server/esm/server.js +62 -22
- package/dist/server/esm/server.js.map +1 -1
- package/package.json +4 -4
|
@@ -334,22 +334,41 @@ 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
|
+
// --- auth header injection with diagnostic logging ---
|
|
346
|
+
const tokenSource = getToken ? 'getToken(injected)' : 'extractTokenFallback';
|
|
347
|
+
const jwt = getToken ? getToken(req) : extractTokenFallback(req);
|
|
348
|
+
// Log incoming auth signals so we can diagnose missing REMOTE_USER / 502s.
|
|
349
|
+
const hasBearerHeader = typeof req.headers['authorization'] === 'string' && req.headers['authorization'].startsWith('Bearer ');
|
|
350
|
+
const cookieHeader = req.headers['cookie'] ?? '';
|
|
351
|
+
const hasAccessTokenCookie = /(?:^|;\s*)access_token=/.test(cookieHeader);
|
|
352
|
+
const cookieNames = cookieHeader.split(';').map((p)=>p.trim().split('=')[0]).filter(Boolean);
|
|
353
|
+
console.warn(`[workspace-assets] proxy auth-diag: tokenSource=${tokenSource} jwtFound=${!!jwt}` + ` hasBearerHeader=${hasBearerHeader} hasAccessTokenCookie=${hasAccessTokenCookie}` + ` cookieNames=[${cookieNames.join(',')}]`);
|
|
354
|
+
if (jwt) {
|
|
355
|
+
const claims = decodeJwtClaims(jwt);
|
|
356
|
+
const rawUsername = claims?.sub ?? claims?.preferred_username ?? claims?.username;
|
|
357
|
+
if (typeof rawUsername === 'string') {
|
|
358
|
+
const safeUsername = rawUsername.replace(/[\r\n]/g, '');
|
|
359
|
+
const safeJwt = jwt.replace(/[\r\n]/g, '');
|
|
360
|
+
forwardHeaders['Authorization'] = `Bearer ${safeJwt}`;
|
|
361
|
+
forwardHeaders['REMOTE_USER'] = safeUsername;
|
|
362
|
+
forwardHeaders['X-Gen3-User-ID'] = safeUsername;
|
|
363
|
+
console.warn(`[workspace-assets] proxy auth-diag: injected REMOTE_USER/X-Gen3-User-ID for sub="${safeUsername}"` + ` claimsKeys=[${Object.keys(claims ?? {}).join(',')}]`);
|
|
364
|
+
} else {
|
|
365
|
+
console.warn(`[workspace-assets] proxy auth-diag: JWT decoded but no sub/preferred_username/username claim found` + ` claimsKeys=[${Object.keys(claims ?? {}).join(',')}]` + ` (REMOTE_USER NOT injected — workspace-proxy will likely reject)`);
|
|
351
366
|
}
|
|
367
|
+
} else {
|
|
368
|
+
console.warn(`[workspace-assets] proxy auth-diag: no JWT found via ${tokenSource}` + ` — REMOTE_USER/X-Gen3-User-ID NOT injected — workspace-proxy will likely 502`);
|
|
352
369
|
}
|
|
370
|
+
// Log the set of header keys being forwarded (no values — headers can contain tokens).
|
|
371
|
+
console.warn(`[workspace-assets] proxy forwarding headers: [${Object.keys(forwardHeaders).join(',')}]`);
|
|
353
372
|
const method = (req.method || 'GET').toUpperCase();
|
|
354
373
|
let body;
|
|
355
374
|
if (![
|
|
@@ -358,7 +377,9 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
|
|
|
358
377
|
].includes(method)) {
|
|
359
378
|
try {
|
|
360
379
|
body = await readBody$2(req);
|
|
361
|
-
} catch
|
|
380
|
+
} catch (error) {
|
|
381
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
382
|
+
console.error(`[workspace-assets] failed to read request body for ${method} ${targetUrl}: ${message}`);
|
|
362
383
|
res.status(500).end('Failed to read request body');
|
|
363
384
|
return;
|
|
364
385
|
}
|
|
@@ -366,7 +387,8 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
|
|
|
366
387
|
forwardHeaders['content-length'] = String(body.length);
|
|
367
388
|
}
|
|
368
389
|
}
|
|
369
|
-
|
|
390
|
+
const startedAt = Date.now();
|
|
391
|
+
console.warn(`[workspace-assets] proxying ${method} ${targetUrl}${body?.length ? ` (body: ${body.length} bytes)` : ''}`);
|
|
370
392
|
await new Promise((resolve)=>{
|
|
371
393
|
const parsed = new URL(targetUrl);
|
|
372
394
|
const transport = parsed.protocol === 'https:' ? https : http;
|
|
@@ -375,10 +397,14 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
|
|
|
375
397
|
hostname: parsed.hostname,
|
|
376
398
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
377
399
|
path: parsed.pathname + parsed.search,
|
|
378
|
-
headers: forwardHeaders
|
|
400
|
+
headers: forwardHeaders,
|
|
401
|
+
// 30-second socket timeout — without this, a stalled upstream holds the
|
|
402
|
+
// connection open indefinitely and the client sees no response.
|
|
403
|
+
timeout: 30_000
|
|
379
404
|
};
|
|
380
405
|
const proxyReq = transport.request(reqOptions, (proxyRes)=>{
|
|
381
|
-
|
|
406
|
+
const statusCode = proxyRes.statusCode ?? 502;
|
|
407
|
+
res.status(statusCode);
|
|
382
408
|
const skip = new Set([
|
|
383
409
|
'transfer-encoding',
|
|
384
410
|
'content-length'
|
|
@@ -388,18 +414,29 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
|
|
|
388
414
|
res.setHeader(key, value);
|
|
389
415
|
}
|
|
390
416
|
}
|
|
417
|
+
const log = statusCode >= 400 ? console.error : console.warn;
|
|
418
|
+
log(`[workspace-assets] ${method} ${targetUrl} -> ${statusCode} (${Date.now() - startedAt}ms)`);
|
|
419
|
+
if (statusCode >= 400) {
|
|
420
|
+
// Log upstream response headers for non-2xx to surface auth/routing rejections.
|
|
421
|
+
const upstreamHeaders = Object.entries(proxyRes.headers).filter(([k])=>!k.toLowerCase().includes('cookie')).map(([k, v])=>`${k}:${Array.isArray(v) ? v.join(',') : v}`).join(' | ');
|
|
422
|
+
console.error(`[workspace-assets] upstream response headers: ${upstreamHeaders}`);
|
|
423
|
+
}
|
|
391
424
|
proxyRes.pipe(res, {
|
|
392
425
|
end: true
|
|
393
426
|
});
|
|
394
427
|
proxyRes.on('end', resolve);
|
|
395
428
|
});
|
|
396
429
|
proxyReq.on('error', (err)=>{
|
|
397
|
-
console.error(
|
|
430
|
+
console.error(`[workspace-assets] extension proxy failed for ${method} ${targetUrl} after ${Date.now() - startedAt}ms: ${err.code ?? 'UNKNOWN'} ${err.message}`);
|
|
398
431
|
if (!res.headersSent) {
|
|
399
432
|
res.status(502).end('Extension proxy unreachable');
|
|
400
433
|
}
|
|
401
434
|
resolve();
|
|
402
435
|
});
|
|
436
|
+
proxyReq.on('timeout', ()=>{
|
|
437
|
+
console.error(`[workspace-assets] extension proxy timed out for ${method} ${targetUrl} after ${Date.now() - startedAt}ms`);
|
|
438
|
+
proxyReq.destroy();
|
|
439
|
+
});
|
|
403
440
|
if (body?.length) {
|
|
404
441
|
proxyReq.write(body);
|
|
405
442
|
}
|
|
@@ -494,7 +531,6 @@ function createWorkspaceAssetsHandler(options) {
|
|
|
494
531
|
return `${proto}://${host}`;
|
|
495
532
|
})();
|
|
496
533
|
const targetUrl = `${proxyBase}${serverExtensionProxyPath}/${extensionPath}${qs}`;
|
|
497
|
-
console.log('[workspace-assets] proxying extension API to', targetUrl);
|
|
498
534
|
return proxyServerExtension(req, res, targetUrl, getToken);
|
|
499
535
|
}
|
|
500
536
|
const allowedFiles = allowedFilesByTier[tier];
|
|
@@ -1185,8 +1221,12 @@ const DEFAULT_OPTIONS = {
|
|
|
1185
1221
|
// Optional internal k8s service base URL for the extension proxy. When set,
|
|
1186
1222
|
// proxy requests target this address directly instead of routing through the
|
|
1187
1223
|
// ingress (avoiding self-signed-cert issues). Readable from the JSON config
|
|
1188
|
-
// file or the
|
|
1189
|
-
serverExtensionProxyBaseUrl:
|
|
1224
|
+
// file or the WORKSPACE_PROXY_URL env var.
|
|
1225
|
+
serverExtensionProxyBaseUrl: process.env.WORKSPACE_PROXY_URL || undefined,
|
|
1226
|
+
// Direct calls to serverExtensionProxyBaseUrl bypass nginx, which normally
|
|
1227
|
+
// injects the X-Gen3-User-ID/REMOTE_USER headers workspace-proxy requires —
|
|
1228
|
+
// reuse this app's cookie extraction to inject them explicitly instead.
|
|
1229
|
+
getToken: (req)=>getAccessToken(req.headers['cookie'] ?? '') ?? null,
|
|
1190
1230
|
// in-browser Pyodide kernel must not be offered alongside it.
|
|
1191
1231
|
additionalDisabledExtensions: [
|
|
1192
1232
|
'@jupyterlite/pyodide-kernel-extension:kernel'
|