@gen3/workspaces 0.12.73 → 0.12.75

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.
@@ -342,18 +342,48 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
342
342
  // Only point serverExtensionProxyBaseUrl at a network path a user's own request cannot
343
343
  // otherwise forge, or add real signature verification (e.g. against Fence's JWKS)
344
344
  // here before trusting these claims.
345
+ // --- auth header injection with diagnostic logging ---
346
+ const tokenSource = getToken ? 'getToken(injected)' : 'extractTokenFallback';
345
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(',')}]`);
346
354
  if (jwt) {
347
355
  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, '');
356
+ // Gen3 nginx extracts the username from context.user.name (not sub, which is
357
+ // a numeric ID). Check context.user.name first so REMOTE_USER matches what
358
+ // workspace-proxy expects for per-user container routing.
359
+ const contextUserName = claims?.context?.user?.name;
360
+ // String username (what Ambassador stores in k8s service names via Hatchery).
361
+ const rawUsername = (typeof contextUserName === 'string' ? contextUserName : undefined) ?? claims?.preferred_username ?? claims?.username;
362
+ // Numeric sub — used as UID in workspace-proxy's uid:<UID>,<username> format.
363
+ const rawSub = claims?.sub;
364
+ const safeSub = typeof rawSub === 'string' ? rawSub.replace(/[\r\n]/g, '') : undefined;
365
+ // workspace-proxy identity.go parses the composite "uid:<UID>,<username>" format:
366
+ // normalizeUsername → extracts the username part (used for k8s service lookup)
367
+ // parseUID → extracts the UID part (used for UID-based fallback lookup)
368
+ // Ambassador normally injects this format; we must replicate it here since we
369
+ // bypass nginx when calling workspace-proxy server-side.
370
+ const safeUsername = typeof rawUsername === 'string' ? rawUsername.replace(/[\r\n]/g, '') : safeSub;
371
+ if (safeUsername !== undefined) {
351
372
  const safeJwt = jwt.replace(/[\r\n]/g, '');
373
+ const remoteUserValue = safeSub && safeUsername !== safeSub ? `uid:${safeSub},${safeUsername}` : safeUsername;
352
374
  forwardHeaders['Authorization'] = `Bearer ${safeJwt}`;
353
- forwardHeaders['REMOTE_USER'] = safeUsername;
354
- forwardHeaders['X-Gen3-User-ID'] = safeUsername;
375
+ forwardHeaders['REMOTE_USER'] = remoteUserValue;
376
+ forwardHeaders['remote_user'] = remoteUserValue;
377
+ forwardHeaders['X-Gen3-User-ID'] = remoteUserValue;
378
+ console.warn(`[workspace-assets] proxy auth-diag: injected REMOTE_USER="${remoteUserValue}"` + ` (contextUserName=${String(contextUserName)} sub=${String(rawSub)})` + ` claimsKeys=[${Object.keys(claims ?? {}).join(',')}]`);
379
+ } else {
380
+ 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)`);
355
381
  }
382
+ } else {
383
+ 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`);
356
384
  }
385
+ // Log the set of header keys being forwarded (no values — headers can contain tokens).
386
+ console.warn(`[workspace-assets] proxy forwarding headers: [${Object.keys(forwardHeaders).join(',')}]`);
357
387
  const method = (req.method || 'GET').toUpperCase();
358
388
  let body;
359
389
  if (![
@@ -382,7 +412,10 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
382
412
  hostname: parsed.hostname,
383
413
  port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
384
414
  path: parsed.pathname + parsed.search,
385
- headers: forwardHeaders
415
+ headers: forwardHeaders,
416
+ // 30-second socket timeout — without this, a stalled upstream holds the
417
+ // connection open indefinitely and the client sees no response.
418
+ timeout: 30_000
386
419
  };
387
420
  const proxyReq = transport.request(reqOptions, (proxyRes)=>{
388
421
  const statusCode = proxyRes.statusCode ?? 502;
@@ -398,6 +431,11 @@ async function proxyServerExtension(req, res, targetUrl, getToken) {
398
431
  }
399
432
  const log = statusCode >= 400 ? console.error : console.warn;
400
433
  log(`[workspace-assets] ${method} ${targetUrl} -> ${statusCode} (${Date.now() - startedAt}ms)`);
434
+ if (statusCode >= 400) {
435
+ // Log upstream response headers for non-2xx to surface auth/routing rejections.
436
+ const upstreamHeaders = Object.entries(proxyRes.headers).filter(([k])=>!k.toLowerCase().includes('cookie')).map(([k, v])=>`${k}:${Array.isArray(v) ? v.join(',') : v}`).join(' | ');
437
+ console.error(`[workspace-assets] upstream response headers: ${upstreamHeaders}`);
438
+ }
401
439
  proxyRes.pipe(res, {
402
440
  end: true
403
441
  });