@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.
@@ -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
- // only add in production mode
338
- // TODO: refactor this to handle development mode better
339
- if (process.env.NODE_ENV === 'production') {
340
- const jwt = getToken ? getToken(req) : extractTokenFallback(req);
341
- if (jwt) {
342
- const claims = decodeJwtClaims(jwt);
343
- const rawUsername = claims?.sub ?? claims?.preferred_username ?? claims?.username;
344
- if (typeof rawUsername === 'string') {
345
- const safeUsername = rawUsername.replace(/[\r\n]/g, '');
346
- const safeJwt = jwt.replace(/[\r\n]/g, '');
347
- forwardHeaders['Authorization'] = `Bearer ${safeJwt}`;
348
- forwardHeaders['REMOTE_USER'] = safeUsername;
349
- forwardHeaders['X-Gen3-User-ID'] = safeUsername;
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
- console.log('[workspace-assets] proxying extension API to', targetUrl);
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
- res.status(proxyRes.statusCode ?? 502);
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('[workspace-assets] extension proxy failed for', targetUrl, err.message);
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 WORKSPACE_EXTENSION_PROXY_BASE_URL env var.
1189
- serverExtensionProxyBaseUrl: 'http://workspace-proxy-service:8080',
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'