diff --git a/backend/.sqlx/query-03b0658a1bc3e2d8831055d336799eca4766ec4f61ae1528e3481fa57c0b3464.json b/backend/.sqlx/query-03b0658a1bc3e2d8831055d336799eca4766ec4f61ae1528e3481fa57c0b3464.json deleted file mode 100644 index 6a616c93b5..0000000000 --- a/backend/.sqlx/query-03b0658a1bc3e2d8831055d336799eca4766ec4f61ae1528e3481fa57c0b3464.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT guest_access_enabled FROM workspace_settings WHERE workspace_id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "guest_access_enabled", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "03b0658a1bc3e2d8831055d336799eca4766ec4f61ae1528e3481fa57c0b3464" -} diff --git a/backend/windmill-common/src/global_settings.rs b/backend/windmill-common/src/global_settings.rs index ce6bc67a54..31223b8b52 100644 --- a/backend/windmill-common/src/global_settings.rs +++ b/backend/windmill-common/src/global_settings.rs @@ -66,7 +66,8 @@ pub const EXPOSE_DEBUG_METRICS_SETTING: &str = "expose_debug_metrics"; pub const KEEP_JOB_DIR_SETTING: &str = "keep_job_dir"; pub const REQUIRE_PREEXISTING_USER_FOR_OAUTH_SETTING: &str = "require_preexisting_user_for_oauth"; /// Superadmin switch over guest sessions for the whole instance, above the per-workspace -/// one. Read from the table, uncached, by the same gates that read the workspace switch. +/// one. Read from the table, uncached, by the same gates that read the workspace switch; +/// the superadmin Guests list writes it through `/settings/global/{key}` by this name. pub const GUEST_ACCESS_DISABLED_SETTING: &str = "guest_access_disabled"; pub const JOB_ISOLATION_SETTING: &str = "job_isolation"; pub const NSJAIL_TMPFS_SIZE_MB_SETTING: &str = "nsjail_tmpfs_size_mb"; diff --git a/backend/windmill-common/src/workspaces.rs b/backend/windmill-common/src/workspaces.rs index 7898d89a19..808c938134 100644 --- a/backend/windmill-common/src/workspaces.rs +++ b/backend/windmill-common/src/workspaces.rs @@ -834,19 +834,22 @@ pub struct GuestUsage { pub guest_seats: i64, } -/// SQL for "the instance admits guests": the superadmin switch, absent meaning on. -const INSTANCE_ADMITS_GUESTS_SQL: &str = - "NOT COALESCE((SELECT value::boolean FROM global_settings \ - WHERE name = 'guest_access_disabled'), false)"; +/// SQL for "the instance admits guests": the superadmin switch, absent meaning on. The +/// setting is read as text before the cast so `true` and `"true"` both count. +fn instance_admits_guests_sql() -> String { + format!( + "NOT COALESCE((SELECT (value #>> '{{}}')::boolean FROM global_settings \ + WHERE name = '{}'), false)", + crate::global_settings::GUEST_ACCESS_DISABLED_SETTING + ) +} pub async fn guest_usage(db: &crate::DB) -> Result { - let instance_enabled: bool = - sqlx::query_scalar(&format!("SELECT {INSTANCE_ADMITS_GUESTS_SQL}")) - .fetch_one(db) - .await - .map_err(|e| { - Error::internal_err(format!("reading the instance guest switch: {e:#}")) - })?; + let instance_admits = instance_admits_guests_sql(); + let instance_enabled: bool = sqlx::query_scalar(&format!("SELECT {instance_admits}")) + .fetch_one(db) + .await + .map_err(|e| Error::internal_err(format!("reading the instance guest switch: {e:#}")))?; let guest_count = guest_count_in_window(db).await?; let metered = guests_are_metered().await; let billable_guests = if metered { @@ -899,32 +902,15 @@ pub async fn guest_admission(conn: &mut sqlx::PgConnection, email: &str) -> Resu ))) } -/// Whether `w_id` admits guest sessions (the `guest` app execution mode). -/// -/// Read uncached where a session is minted ([`guest_app_admits`]) and then once per -/// request at the auth door (`AuthCache::get_opt_job_authed`) for every guest. An app -/// carries its own `execution_mode` in its definition, so git-sync and the CLI can -/// push `guest` past every deploy-time gate; the per-request read is what makes -/// turning the switch off take effect on sessions already issued. -pub async fn is_guest_access_enabled(db: &crate::DB, w_id: &str) -> Result { - Ok(sqlx::query_scalar!( - "SELECT guest_access_enabled FROM workspace_settings WHERE workspace_id = $1", - w_id - ) - .fetch_optional(db) - .await - .map_err(|e| Error::internal_err(format!("reading guest access of {w_id}: {e:#}")))? - .unwrap_or(false)) -} - /// Whether a guest session for `email` in `w_id` still stands: the instance and the /// workspace admit guests, and the email still has no account. Read at the auth door on /// every guest request, so turning either switch off, or an account provisioned after /// the mint (or racing it), ends the session on its next request. pub async fn guest_session_stands(db: &crate::DB, w_id: &str, email: &str) -> Result { + let instance_admits = instance_admits_guests_sql(); let stands: Option = sqlx::query_scalar(&format!( "SELECT guest_access_enabled - AND {INSTANCE_ADMITS_GUESTS_SQL} + AND {instance_admits} AND NOT EXISTS(SELECT 1 FROM password WHERE email = $2) AND NOT EXISTS(SELECT 1 FROM usr WHERE email = $2) FROM workspace_settings WHERE workspace_id = $1" @@ -947,9 +933,10 @@ pub async fn guest_app_admits<'c, E: sqlx::Executor<'c, Database = sqlx::Postgre w_id: &str, app_path: &str, ) -> Result { + let instance_admits = instance_admits_guests_sql(); let admits: Option = sqlx::query_scalar(&format!( "SELECT COALESCE(ws.guest_access_enabled AND app.policy->>'execution_mode' = 'guest', false) - AND {INSTANCE_ADMITS_GUESTS_SQL} + AND {instance_admits} FROM app JOIN workspace_settings ws ON ws.workspace_id = app.workspace_id WHERE app.workspace_id = $1 AND app.path = $2" )) diff --git a/frontend/src/lib/components/SuperadminSettingsInner.svelte b/frontend/src/lib/components/SuperadminSettingsInner.svelte index fe9ec97a37..51ceed9aba 100644 --- a/frontend/src/lib/components/SuperadminSettingsInner.svelte +++ b/frontend/src/lib/components/SuperadminSettingsInner.svelte @@ -56,6 +56,7 @@ import InstanceAISettings from './instanceSettings/InstanceAISettings.svelte' import ExternalJwtTokens from './instanceSettings/ExternalJwtTokens.svelte' import GuestActivityList from './instanceSettings/GuestActivityList.svelte' + import { Alert, Skeleton } from '$lib/components/common' let filter = $state('') @@ -370,7 +371,7 @@ - {#if usersSubTab === 'users' || (usersSubTab === 'ext_jwt' && extJwtTokens.length === 0) || (usersSubTab === 'guests' && !guestList)} + {#if usersSubTab === 'users' || (usersSubTab === 'ext_jwt' && extJwtTokens.length === 0)} + {:else if usersSubTab === 'guests' && !guestList} + {#if guestLoading} + + {:else} + + + + {/if} {:else if usersSubTab === 'guests' && guestList}
- setInstanceSwitch(e.detail)} - options={{ - right: 'Allow guests on this instance', - rightTooltip: - 'Off, no guest can sign in anywhere, whatever a workspace or an app says, and sessions already issued stop on their next request.' - }} - /> + {#key usage} + setInstanceSwitch(e.detail)} + options={{ + right: 'Allow guests on this instance', + rightTooltip: + 'Off, no guest can sign in anywhere, whatever a workspace or an app says, and sessions already issued stop on their next request.' + }} + /> + {/key}
diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index 5b5f614929..6e55c5994b 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -526,15 +526,17 @@ } async function saveDefaultAppSettings(): Promise { + // Guests first: the only write of this card available on every plan, so a refused + // Enterprise-only write after it cannot swallow it. + if (guestAccessEnabled !== initialGuestAccessEnabled) { + await editGuestAccess() + } if (workspaceDefaultAppPath !== initialWorkspaceDefaultAppPath) { await editWorkspaceDefaultApp() } if (publicAppRateLimitPerMinute !== initialPublicAppRateLimitPerMinute) { await editPublicAppRateLimit() } - if (guestAccessEnabled !== initialGuestAccessEnabled) { - await editGuestAccess() - } } async function editGuestAccess(): Promise {