mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix: CLI falls back to workspace whoami for workspace-scoped tokens (#8789)
* fix: CLI falls back to workspace whoami when global whoami is 401
Workspace-scoped tokens (token.workspace_id set) cannot call
/api/users/whoami — the backend's token lookup filters by workspace_id
which is NULL on global paths, so auth returns 401 before the handler
runs. This breaks the CLI entirely: requireLogin calls globalWhoami at
the start of every command, so no command works with a
workspace-scoped token, not even `wmill workspace whoami`.
Fix it CLI-side: if the global whoami returns 401, fall back to the
workspace-scoped /api/w/{w}/users/whoami using the workspace already
known from the CLI profile, and adapt the response shape to
GlobalUserInfo. Also drop the redundant second globalWhoami call in
`wmill workspace whoami` — use requireLogin's return value instead.
No backend changes: the workspace_id binding on the token stays
strictly enforced for every global endpoint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use name-based ApiError check in whoami fallback
Review feedback from PR #8789: `instanceof ApiError` can silently
return false when bundling produces multiple module instances of
`gen/core/ApiError.ts` (bun build for npm, JSR dev path), which would
skip the workspace-whoami fallback and reintroduce the exact bug this
PR fixes. Match the name-based check already used at
`cli/src/main.ts:232` and drop the `ApiError` import.
Also add a comment on `workspaceUserToGlobalUserInfo` listing the
fields that aren't derivable from the workspace-scoped User response
and are filled with placeholder values, so future callers don't trust
them downstream.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a7512f9034
commit
d243eb31b0
@@ -412,8 +412,7 @@ async function remove(_opts: GlobalOptions, name: string) {
|
||||
}
|
||||
|
||||
async function whoami(_opts: GlobalOptions) {
|
||||
await requireLogin(_opts);
|
||||
const whoamiInfo = await wmill.globalWhoami();
|
||||
const whoamiInfo = await requireLogin(_opts);
|
||||
log.info(JSON.stringify(whoamiInfo, null, 2));
|
||||
const activeName = await getActiveWorkspaceName(_opts);
|
||||
const { getCurrentGitBranch, getOriginalBranchForWorkspaceForks } = await import("../../utils/git.ts");
|
||||
|
||||
+47
-3
@@ -2,12 +2,56 @@ import { colors } from "@cliffy/ansi/colors";
|
||||
import * as log from "./log.ts";
|
||||
import { setClient } from "./client.ts";
|
||||
import * as wmill from "../../gen/services.gen.ts";
|
||||
import { GlobalUserInfo } from "../../gen/types.gen.ts";
|
||||
import { GlobalUserInfo, User } from "../../gen/types.gen.ts";
|
||||
|
||||
import { loginInteractive, tryGetLoginInfo } from "./login.ts";
|
||||
import { GlobalOptions } from "../types.ts";
|
||||
|
||||
|
||||
// Workspace-scoped tokens (tokens bound to a workspace via token.workspace_id)
|
||||
// cannot call /api/users/whoami because the backend rejects any token with a
|
||||
// workspace binding when the request path has no workspace in it. Fall back to
|
||||
// the workspace-scoped whoami endpoint in that case so the CLI still works.
|
||||
// Uses a name-based ApiError check rather than `instanceof` to match the
|
||||
// pattern in cli/src/main.ts: bundling (bun build for npm, JSR dev path) can
|
||||
// produce multiple module instances of gen/core/ApiError.ts, making
|
||||
// `instanceof` silently return false and reintroducing the bug this fixes.
|
||||
async function fetchWhoami(workspaceId: string): Promise<GlobalUserInfo> {
|
||||
try {
|
||||
return await wmill.globalWhoami();
|
||||
} catch (error) {
|
||||
if (
|
||||
error && typeof error === "object" &&
|
||||
"name" in error && (error as { name: unknown }).name === "ApiError" &&
|
||||
(error as { status?: number }).status === 401
|
||||
) {
|
||||
const user = await wmill.whoami({ workspace: workspaceId });
|
||||
return workspaceUserToGlobalUserInfo(user);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Adapter for the 401 fallback path. `login_type`, `verified`, `first_time_user`
|
||||
// and `role_source` are NOT derivable from the workspace-scoped User response
|
||||
// and are filled with best-effort defaults — do not trust them downstream after
|
||||
// a fallback whoami. Today only cli/src/commands/hub/hub.ts reads this return
|
||||
// value, and only `.email`.
|
||||
function workspaceUserToGlobalUserInfo(user: User): GlobalUserInfo {
|
||||
return {
|
||||
email: user.email,
|
||||
login_type: "password",
|
||||
super_admin: user.is_super_admin,
|
||||
verified: true,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
operator_only: user.operator,
|
||||
first_time_user: false,
|
||||
role_source: "manual",
|
||||
disabled: user.disabled,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Main authentication function - moved from context.ts to break circular dependencies
|
||||
* This function maintains the original API signature from context.ts
|
||||
@@ -28,7 +72,7 @@ export async function requireLogin(
|
||||
setClient(token, workspace.remote.substring(0, workspace.remote.length - 1));
|
||||
|
||||
try {
|
||||
return await wmill.globalWhoami();
|
||||
return await fetchWhoami(workspace.workspaceId);
|
||||
} catch (error) {
|
||||
// Check for network errors and provide clearer messages
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
@@ -62,6 +106,6 @@ export async function requireLogin(
|
||||
newToken,
|
||||
workspace.remote.substring(0, workspace.remote.length - 1)
|
||||
);
|
||||
return await wmill.globalWhoami();
|
||||
return await fetchWhoami(workspace.workspaceId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user