fix: drop the dead guest-access helper, name the instance setting once, guests tab states, CE save order

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BayTppRCstWX6qTf3LMco5
This commit is contained in:
Ruben Fiszel
2026-09-03 22:09:33 +02:00
co-authored by Claude Fable 5.1
parent f626dc312f
commit 07dfa0ba8b
6 changed files with 54 additions and 67 deletions
@@ -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"
}
@@ -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";
+18 -31
View File
@@ -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<GuestUsage> {
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<bool> {
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<bool> {
let instance_admits = instance_admits_guests_sql();
let stands: Option<bool> = 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<bool> {
let instance_admits = instance_admits_guests_sql();
let admits: Option<bool> = 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"
))
@@ -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 @@
<Tab value="guests" label="Guests" />
</Tabs>
{#if usersSubTab === 'users' || (usersSubTab === 'ext_jwt' && extJwtTokens.length === 0) || (usersSubTab === 'guests' && !guestList)}
{#if usersSubTab === 'users' || (usersSubTab === 'ext_jwt' && extJwtTokens.length === 0)}
<SettingsPageHeader
title="Instance users ({users.length})"
description="Manage all users across your Windmill instance."
@@ -736,6 +737,16 @@
loadExtJwtPage(1)
}}
/>
{:else if usersSubTab === 'guests' && !guestList}
{#if guestLoading}
<Skeleton layout={[[2], 1, [8]]} />
{:else}
<Alert type="error" title="Could not load guests">
<Button unifiedSize="sm" variant="default" onclick={() => loadGuestPage(1)}
>Retry</Button
>
</Alert>
{/if}
{:else if usersSubTab === 'guests' && guestList}
<GuestActivityList
usage={guestList.usage}
@@ -20,8 +20,12 @@
let { usage, guests, hasMore, loading, onLoadMore, onInstanceSwitch }: Props = $props()
const loadMoreSize = 50
// One write at a time, and the toggle shows the stored value again after either
// outcome: a refused write must not leave it showing the click.
let switchPending = $state(false)
async function setInstanceSwitch(enabled: boolean) {
switchPending = true
try {
await SettingService.setGlobal({
key: 'guest_access_disabled',
@@ -35,6 +39,7 @@
} catch (e) {
sendUserToast(`Could not change the instance guest switch: ${e}`, true)
}
switchPending = false
onInstanceSwitch()
}
// A capped instance refuses the next stranger as soon as the allowance is used up.
@@ -51,15 +56,18 @@
/>
<div class="flex flex-row gap-2 items-center mb-4">
<Toggle
checked={usage.instance_enabled}
on:change={(e) => 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}
<Toggle
checked={usage.instance_enabled}
disabled={switchPending}
on:change={(e) => 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}
</div>
<div class="mb-4">
@@ -526,15 +526,17 @@
}
async function saveDefaultAppSettings(): Promise<void> {
// 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<void> {