From 6056ec7148bce9f8ed171dd29f544696c335de7d Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 11 Sep 2026 13:19:52 +0200 Subject: [PATCH] feat: let apps hide the viewer login status on public urls (#11089) * feat: let apps hide the viewer login status on public urls Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FTWrfHeqcFMH8qWdsP6kEr * fix: apply the login status setting on deploy and regenerate mcp tools Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FTWrfHeqcFMH8qWdsP6kEr * fix: save the login status toggle immediately like its sibling toggles Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FTWrfHeqcFMH8qWdsP6kEr --------- Co-authored-by: Claude Opus 5 (1M context) --- backend/windmill-api/openapi.yaml | 6 ++++++ backend/windmill-api/src/apps.rs | 6 ++++++ .../src/mcp/auto_generated_endpoints.rs | 12 +++++++++-- .../apps/editor/AppEditorHeaderDeploy.svelte | 18 +++++++++++++++++ .../components/apps/editor/PublicApp.svelte | 20 ++++++++++++------- frontend/src/lib/mcpEndpointTools.ts | 12 +++++++++-- 6 files changed, 63 insertions(+), 11 deletions(-) diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index a43135d440..96b78cc78d 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -33969,6 +33969,12 @@ components: the app bundle so `windmill-client` calls run as the viewer. Must be a subset of the server's curated allowlist (jobs:run, jobs:read, users:read, resources:read, variables:read). + hide_login_status: + type: boolean + description: > + When true, the app's public and custom URLs do not show the viewer's + login status (the user they are signed in as, or that they are + signed out) in the top-left corner. Absent or false shows it. ListableApp: type: object diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index 25178ebd80..183a3a7ec7 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -521,6 +521,9 @@ pub struct Policy { /// `FRONTEND_SDK_ALLOWED_SCOPES`; absent means no credential (the default). #[serde(skip_serializing_if = "Option::is_none")] pub frontend_sdk_scopes: Option>, + /// Display only: hides the viewer's login status badge on the public viewer. + #[serde(skip_serializing_if = "Option::is_none")] + pub hide_login_status: Option, } impl Policy { @@ -4580,6 +4583,7 @@ async fn upload_s3_file_from_app( allowed_s3_keys: None, sandbox: None, frontend_sdk_scopes: None, + hide_login_status: None, }) } else { let policy_o = sqlx::query_scalar!( @@ -4995,6 +4999,7 @@ async fn get_on_behalf_authed_from_app( allowed_s3_keys: Some(force_allowed_s3_keys), sandbox: None, frontend_sdk_scopes: None, + hide_login_status: None, } } else { // TODO: improve db query to not return uneeded fields @@ -5019,6 +5024,7 @@ async fn get_on_behalf_authed_from_app( allowed_s3_keys: None, sandbox: None, frontend_sdk_scopes: None, + hide_login_status: None, }) }; diff --git a/backend/windmill-api/src/mcp/auto_generated_endpoints.rs b/backend/windmill-api/src/mcp/auto_generated_endpoints.rs index aa2415f664..17351776bc 100644 --- a/backend/windmill-api/src/mcp/auto_generated_endpoints.rs +++ b/backend/windmill-api/src/mcp/auto_generated_endpoints.rs @@ -1265,7 +1265,7 @@ is, a different one moves it there and archives the old path"), }, "execution_mode": { "type": "string", - "description": "Who the app's runnables execute as. Optional, and what omitting it means depends on the operation: creating an app defaults it to `publisher` (runs on behalf of the app's publisher and requires an authenticated viewer), while updating one keeps the mode the app is already deployed under. Either way `anonymous`, which makes the app publicly executable, is never assumed. Possible values: viewer, publisher, anonymous" + "description": "Who may open the app, and who its runnables execute as. Optional, and what omitting it means depends on the operation: creating an app defaults it to `publisher` (runs on behalf of the app's publisher and requires an authenticated viewer), while updating one keeps the mode the app is already deployed under. Neither `anonymous`, which makes the app publicly executable, nor `guest`, which opens it to anyone the identity provider authenticates, is ever assumed. A guest is only admitted where the workspace also has `guest_access_enabled`, which is checked when the session is minted and again on every guest request. Possible values: viewer, publisher, guest, anonymous" }, "on_behalf_of": { "type": "string" @@ -1283,6 +1283,10 @@ is, a different one moves it there and archives the old path"), "type": "string" }, "description": "Raw apps: author-declared scopes for the frontend SDK token. Takes effect only when `sandbox` is also true — an unsandboxed bundle runs with the viewer's own session, so no token is advertised or minted for it and this list stays inert. On a sandboxed app a non-empty list lets viewers mint (after consenting) a short-lived token carrying their own identity restricted to these scopes, handed to the app bundle so `windmill-client` calls run as the viewer. Must be a subset of the server's curated allowlist (jobs:run, jobs:read, users:read, resources:read, variables:read).\n" + }, + "hide_login_status": { + "type": "boolean", + "description": "When true, the app's public and custom URLs do not show the viewer's login status (the user they are signed in as, or that they are signed out) in the top-left corner. Absent or false shows it.\n" } } } @@ -1380,7 +1384,7 @@ is, a different one moves it there and archives the old path"), }, "execution_mode": { "type": "string", - "description": "Who the app's runnables execute as. Optional, and what omitting it means depends on the operation: creating an app defaults it to `publisher` (runs on behalf of the app's publisher and requires an authenticated viewer), while updating one keeps the mode the app is already deployed under. Either way `anonymous`, which makes the app publicly executable, is never assumed. Possible values: viewer, publisher, anonymous" + "description": "Who may open the app, and who its runnables execute as. Optional, and what omitting it means depends on the operation: creating an app defaults it to `publisher` (runs on behalf of the app's publisher and requires an authenticated viewer), while updating one keeps the mode the app is already deployed under. Neither `anonymous`, which makes the app publicly executable, nor `guest`, which opens it to anyone the identity provider authenticates, is ever assumed. A guest is only admitted where the workspace also has `guest_access_enabled`, which is checked when the session is minted and again on every guest request. Possible values: viewer, publisher, guest, anonymous" }, "on_behalf_of": { "type": "string" @@ -1398,6 +1402,10 @@ is, a different one moves it there and archives the old path"), "type": "string" }, "description": "Raw apps: author-declared scopes for the frontend SDK token. Takes effect only when `sandbox` is also true — an unsandboxed bundle runs with the viewer's own session, so no token is advertised or minted for it and this list stays inert. On a sandboxed app a non-empty list lets viewers mint (after consenting) a short-lived token carrying their own identity restricted to these scopes, handed to the app bundle so `windmill-client` calls run as the viewer. Must be a subset of the server's curated allowlist (jobs:run, jobs:read, users:read, resources:read, variables:read).\n" + }, + "hide_login_status": { + "type": "boolean", + "description": "When true, the app's public and custom URLs do not show the viewer's login status (the user they are signed in as, or that they are signed out) in the top-left corner. Absent or false shows it.\n" } } }, diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte index 836af68db6..733f4134b6 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte @@ -649,6 +649,24 @@ {/if} + +
+ { + policy.hide_login_status = e.detail ? undefined : true + if (savedApp && !newApp) { + setPublishState(e.detail ? 'Login status shown' : 'Login status hidden') + } + }} + disabled={!savedApp} + /> +
+ The public and custom URLs show who the viewer is signed in as, or that they are signed out, + in the top-left corner. +
+
You will still need to deploy the app to make visible the latest changes diff --git a/frontend/src/lib/components/apps/editor/PublicApp.svelte b/frontend/src/lib/components/apps/editor/PublicApp.svelte index f807520c27..af032f02dc 100644 --- a/frontend/src/lib/components/apps/editor/PublicApp.svelte +++ b/frontend/src/lib/components/apps/editor/PublicApp.svelte @@ -57,6 +57,10 @@ // Use workspace from props or from app.workspace_id (for custom path responses) let effectiveWorkspace = $derived(workspace ?? app?.workspace_id) + // The setting lives on the app, so the badge waits for it while loading + // instead of flashing on an app that hides it. + let showLoginStatus = $derived(app ? !app.policy?.hide_login_status : notExists || noPermission) + // On the public surfaces (untrusted distribution) runnable-authored html/svg needs // the viewer's approval before it renders, unless the app sandbox isolates it. The // in-workspace viewer renders it verbatim. See getAppMarkupTrust. @@ -117,13 +121,15 @@
{child}
{/snippet} -
{#if $userStore} - {@render userInfo($userStore.username)} - {:else if globalUser} - {@render userInfo(globalUser.email)} - {:else}{/if} -
+ {#if showLoginStatus} +
{#if $userStore} + {@render userInfo($userStore.username)} + {:else if globalUser} + {@render userInfo(globalUser.email)} + {:else}{/if} +
+ {/if} {/if} {#if notExists} diff --git a/frontend/src/lib/mcpEndpointTools.ts b/frontend/src/lib/mcpEndpointTools.ts index ce4adf82be..d3da430240 100644 --- a/frontend/src/lib/mcpEndpointTools.ts +++ b/frontend/src/lib/mcpEndpointTools.ts @@ -1272,7 +1272,7 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "execution_mode": { "type": "string", - "description": "Who the app's runnables execute as. Optional, and what omitting it means depends on the operation: creating an app defaults it to `publisher` (runs on behalf of the app's publisher and requires an authenticated viewer), while updating one keeps the mode the app is already deployed under. Either way `anonymous`, which makes the app publicly executable, is never assumed. Possible values: viewer, publisher, anonymous" + "description": "Who may open the app, and who its runnables execute as. Optional, and what omitting it means depends on the operation: creating an app defaults it to `publisher` (runs on behalf of the app's publisher and requires an authenticated viewer), while updating one keeps the mode the app is already deployed under. Neither `anonymous`, which makes the app publicly executable, nor `guest`, which opens it to anyone the identity provider authenticates, is ever assumed. A guest is only admitted where the workspace also has `guest_access_enabled`, which is checked when the session is minted and again on every guest request. Possible values: viewer, publisher, guest, anonymous" }, "on_behalf_of": { "type": "string" @@ -1290,6 +1290,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "type": "string" }, "description": "Raw apps: author-declared scopes for the frontend SDK token. Takes effect only when `sandbox` is also true \u2014 an unsandboxed bundle runs with the viewer's own session, so no token is advertised or minted for it and this list stays inert. On a sandboxed app a non-empty list lets viewers mint (after consenting) a short-lived token carrying their own identity restricted to these scopes, handed to the app bundle so `windmill-client` calls run as the viewer. Must be a subset of the server's curated allowlist (jobs:run, jobs:read, users:read, resources:read, variables:read).\n" + }, + "hide_login_status": { + "type": "boolean", + "description": "When true, the app's public and custom URLs do not show the viewer's login status (the user they are signed in as, or that they are signed out) in the top-left corner. Absent or false shows it.\n" } } } @@ -1387,7 +1391,7 @@ export const mcpEndpointTools: EndpointTool[] = [ }, "execution_mode": { "type": "string", - "description": "Who the app's runnables execute as. Optional, and what omitting it means depends on the operation: creating an app defaults it to `publisher` (runs on behalf of the app's publisher and requires an authenticated viewer), while updating one keeps the mode the app is already deployed under. Either way `anonymous`, which makes the app publicly executable, is never assumed. Possible values: viewer, publisher, anonymous" + "description": "Who may open the app, and who its runnables execute as. Optional, and what omitting it means depends on the operation: creating an app defaults it to `publisher` (runs on behalf of the app's publisher and requires an authenticated viewer), while updating one keeps the mode the app is already deployed under. Neither `anonymous`, which makes the app publicly executable, nor `guest`, which opens it to anyone the identity provider authenticates, is ever assumed. A guest is only admitted where the workspace also has `guest_access_enabled`, which is checked when the session is minted and again on every guest request. Possible values: viewer, publisher, guest, anonymous" }, "on_behalf_of": { "type": "string" @@ -1405,6 +1409,10 @@ export const mcpEndpointTools: EndpointTool[] = [ "type": "string" }, "description": "Raw apps: author-declared scopes for the frontend SDK token. Takes effect only when `sandbox` is also true \u2014 an unsandboxed bundle runs with the viewer's own session, so no token is advertised or minted for it and this list stays inert. On a sandboxed app a non-empty list lets viewers mint (after consenting) a short-lived token carrying their own identity restricted to these scopes, handed to the app bundle so `windmill-client` calls run as the viewer. Must be a subset of the server's curated allowlist (jobs:run, jobs:read, users:read, resources:read, variables:read).\n" + }, + "hide_login_status": { + "type": "boolean", + "description": "When true, the app's public and custom URLs do not show the viewer's login status (the user they are signed in as, or that they are signed out) in the top-left corner. Absent or false shows it.\n" } } },