From 8922be11a6935d200e1909f14aa410c6eff67a13 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Wed, 26 Nov 2025 12:02:21 +0100 Subject: [PATCH] fix(frontend): workspace color race condition for superadmins (#7229) --- ...3cebdf8d777f48e148bcb171fa15e8bf6f098.json | 58 +++++++++++++++++++ backend/windmill-api/openapi.yaml | 16 +++++ backend/windmill-api/src/workspaces.rs | 41 ++++++++++++- frontend/src/lib/stores.ts | 28 ++++++--- 4 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 backend/.sqlx/query-00c1dd0cfaf15aafdcfcabc1f123cebdf8d777f48e148bcb171fa15e8bf6f098.json diff --git a/backend/.sqlx/query-00c1dd0cfaf15aafdcfcabc1f123cebdf8d777f48e148bcb171fa15e8bf6f098.json b/backend/.sqlx/query-00c1dd0cfaf15aafdcfcabc1f123cebdf8d777f48e148bcb171fa15e8bf6f098.json new file mode 100644 index 0000000000..72942b3788 --- /dev/null +++ b/backend/.sqlx/query-00c1dd0cfaf15aafdcfcabc1f123cebdf8d777f48e148bcb171fa15e8bf6f098.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n workspace.id AS \"id!\",\n workspace.name AS \"name!\",\n workspace.owner AS \"owner!\",\n workspace.deleted AS \"deleted!\",\n workspace.premium AS \"premium!\",\n workspace_settings.color AS \"color\",\n workspace.parent_workspace_id AS \"parent_workspace_id\"\n FROM workspace\n LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id\n WHERE workspace.id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id!", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "name!", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "owner!", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "deleted!", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "premium!", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "color", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "parent_workspace_id", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true + ] + }, + "hash": "00c1dd0cfaf15aafdcfcabc1f123cebdf8d777f48e148bcb171fa15e8bf6f098" +} diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 139c6f325a..9ce19ef33a 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -688,6 +688,22 @@ paths: schema: $ref: "#/components/schemas/UserWorkspaceList" + /w/{workspace}/workspaces/get_as_superadmin: + get: + summary: get workspace as super admin (require to be super admin) + operationId: getWorkspaceAsSuperAdmin + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + responses: + "200": + description: workspace + content: + application/json: + schema: + $ref: "#/components/schemas/Workspace" + /workspaces/list_as_superadmin: get: summary: list all workspaces as super admin (require to be super admin) diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index cd624e7c9d..ebc53eb733 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -50,7 +50,7 @@ use windmill_common::{ oauth2::WORKSPACE_SLACK_BOT_TOKEN_PATH, utils::{paginate, rd_string, require_admin, Pagination}, }; -use windmill_git_sync::{handle_fork_branch_creation, handle_deployment_metadata, DeployedObject}; +use windmill_git_sync::{handle_deployment_metadata, handle_fork_branch_creation, DeployedObject}; use windmill_worker::scoped_dependency_map::{DependencyMap, ScopedDependencyMap}; #[cfg(feature = "enterprise")] @@ -73,6 +73,7 @@ lazy_static::lazy_static! { pub fn workspaced_service() -> Router { let router = Router::new() + .route("/get_as_superadmin", get(get_workspace_as_superadmin)) .route("/list_pending_invites", get(list_pending_invites)) .route("/update", post(edit_workspace)) .route("/archive", post(archive_workspace)) @@ -163,7 +164,10 @@ pub fn workspaced_service() -> Router { post(acknowledge_all_critical_alerts), ) .route("/critical_alerts/mute", post(mute_critical_alerts)) - .route("/create_workspace_fork_branch", post(create_workspace_fork_branch)) + .route( + "/create_workspace_fork_branch", + post(create_workspace_fork_branch), + ) .route("/operator_settings", post(update_operator_settings)); #[cfg(all(feature = "stripe", feature = "enterprise"))] @@ -706,7 +710,9 @@ async fn get_slack_oauth_config( .await?; // Mask the secret if it exists - let masked_secret = settings.slack_oauth_client_secret.map(|_| "***".to_string()); + let masked_secret = settings + .slack_oauth_client_secret + .map(|_| "***".to_string()); Ok(Json(GetSlackOAuthConfigResponse { slack_oauth_client_id: settings.slack_oauth_client_id, @@ -2227,6 +2233,35 @@ async fn get_used_triggers( Ok(Json(websocket_used)) } +async fn get_workspace_as_superadmin( + authed: ApiAuthed, + Extension(db): Extension, + Path(w_id): Path, +) -> JsonResult { + require_super_admin(&db, &authed.email).await?; + let workspace = sqlx::query_as!( + Workspace, + "SELECT + workspace.id AS \"id!\", + workspace.name AS \"name!\", + workspace.owner AS \"owner!\", + workspace.deleted AS \"deleted!\", + workspace.premium AS \"premium!\", + workspace_settings.color AS \"color\", + workspace.parent_workspace_id AS \"parent_workspace_id\" + FROM workspace + LEFT JOIN workspace_settings ON workspace.id = workspace_settings.workspace_id + WHERE workspace.id = $1", + w_id + ) + .fetch_optional(&db) + .await?; + + let workspace = not_found_if_none(workspace, "workspace", w_id)?; + + Ok(Json(workspace)) +} + async fn list_workspaces_as_super_admin( authed: ApiAuthed, Extension(db): Extension, diff --git a/frontend/src/lib/stores.ts b/frontend/src/lib/stores.ts index 91ecfd1262..5632bff47a 100644 --- a/frontend/src/lib/stores.ts +++ b/frontend/src/lib/stores.ts @@ -3,9 +3,11 @@ import { derived, type Readable, writable } from 'svelte/store' import type { IntrospectionQuery } from 'graphql' import { + CancelablePromise, type OperatorSettings, type TokenResponse, type UserWorkspaceList, + type Workspace, type WorkspaceDefaultScripts, WorkspaceService } from './gen' @@ -168,32 +170,40 @@ export const instanceSettingsSelectedTab = writable('Core') export const isCriticalAlertsUIOpen = writable(false) +let getWorkspacePromise: CancelablePromise | null = null export const workspaceColor: Readable = derived( [workspaceStore, usersWorkspaceStore, superadmin], ([workspaceStore, usersWorkspaceStore, superadmin], set: (value: string | undefined) => void) => { - if (!workspaceStore) { - set(undefined) + if (!workspaceStore || !usersWorkspaceStore) { return } // First try to get the color from usersWorkspaceStore - const color = usersWorkspaceStore?.workspaces.find((w) => w.id === workspaceStore)?.color + const workspace = usersWorkspaceStore.workspaces.find((w) => w.id === workspaceStore) - if (color) { - set(color) + if (workspace) { + set(workspace.color) return } - // If not found and user is superadmin, try to get it from superadmin list + // If workspace not found and user is superadmin, get it as superadmin if (!superadmin) { set(undefined) return } - WorkspaceService.listWorkspacesAsSuperAdmin().then((workspaces) => { - const superadminColor = workspaces.find((w) => w.id === workspaceStore)?.color - set(superadminColor) + getWorkspacePromise?.cancel() + + getWorkspacePromise = WorkspaceService.getWorkspaceAsSuperAdmin({ + workspace: workspaceStore }) + + getWorkspacePromise + .then((workspace) => set(workspace.color)) + .catch((error) => { + console.error('error getting workspace as superadmin', error) + set(undefined) + }) } )