mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 00:01:49 +00:00
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:
co-authored by
Claude Fable 5.1
parent
f626dc312f
commit
07dfa0ba8b
-22
@@ -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";
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user