fix: the guest plan gate fails closed on non-enterprise builds; settings report the effective switch

This commit is contained in:
Ruben Fiszel
2026-09-02 17:17:49 +00:00
parent 6e2f7d1530
commit 87a3ed9766
4 changed files with 24 additions and 17 deletions
+3 -1
View File
@@ -15,7 +15,9 @@
//! is written (git-sync and the CLI push policies past every UI).
//!
//! The token is inserted directly: how a guest session is minted is the identity
//! provider's business (EE), what one can do is this file's.
//! provider's business (EE), what one can do is this file's. Build with
//! `enterprise,private`, as CI does: the plan gate refuses every guest on a build
//! without them, so on a plain CE build every test here fails at the door.
//!
//! Users from the `base` fixture:
//! test-user (admin, token SECRET_TOKEN)
@@ -1099,6 +1099,10 @@ async fn get_settings(
if let Some(git_sync) = settings.git_sync.as_mut() {
redact_git_sync_webhook_secrets(git_sync);
}
// The effective value, not the column: a switch left on by a plan that no longer
// admits guests must not read as open when every gate says shut.
settings.guest_access_enabled =
settings.guest_access_enabled && windmill_common::workspaces::guest_access_licensed().await;
Ok(Json(settings))
}
@@ -1135,8 +1139,10 @@ async fn get_public_settings(
.await
.map_err(|e| Error::internal_err(format!("getting public settings: {e:#}")))?;
let settings = not_found_if_none(settings, "workspace settings", &w_id)?;
let mut settings = not_found_if_none(settings, "workspace settings", &w_id)?;
tx.commit().await?;
settings.guest_access_enabled =
settings.guest_access_enabled && windmill_common::workspaces::guest_access_licensed().await;
Ok(Json(settings))
}
+8 -7
View File
@@ -775,10 +775,10 @@ pub struct BillableSeats {
pub seats: i64,
}
/// Guests are an Enterprise-plan feature, so a Pro key refuses them at every gate —
/// the workspace switch, the mint, the door — not only in the UI that hides them. A
/// build without `enterprise` has no plan to consult and cannot mint a guest session
/// at all (the mint lives in EE code), so nothing is gated there.
/// Guests are an Enterprise-plan feature, refused on any other plan and on any build
/// without `enterprise` at every gate: the switch write, discovery, the mint, the
/// door. The CE image compiles the OAuth callback that mints, so the build check is
/// load-bearing, not a formality.
pub async fn guest_access_licensed() -> bool {
#[cfg(feature = "enterprise")]
{
@@ -789,12 +789,13 @@ pub async fn guest_access_licensed() -> bool {
}
#[cfg(not(feature = "enterprise"))]
{
true
false
}
}
/// Whether `w_id` admits guest sessions — someone the identity provider authenticated
/// who is a member of no workspace, and who therefore takes no seat.
/// who is a member of no workspace, and who therefore takes no seat. The plan is
/// checked first: a switch left on by a plan that no longer admits guests is shut.
///
/// 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
@@ -815,7 +816,7 @@ pub async fn is_guest_access_enabled(db: &crate::DB, w_id: &str) -> Result<bool>
.unwrap_or(false))
}
/// Both gates at once: the workspace admits guests and `app_path` is in `guest`
/// Every gate at once: the plan, the workspace switch, and `app_path` being in `guest`
/// execution mode. The single answer to "may a guest session be minted for this app",
/// used by the mint itself and by the sign-in branch that decides whether to call it.
/// A missing app or a policy with no stated mode reads as "no".
@@ -215,10 +215,9 @@
let accountSession = $state<'unknown' | 'none' | 'held'>('unknown')
let deniedStatus: number | undefined = $state(undefined)
/** The sign-in card belongs on a 401, and on a 403 unless discovery has settled
* that the app is not open to guests a 403 on a guest app is a session for a
* different app of the same workspace, which a fresh sign-in replaces. While
* discovery is still pending this stays true so the skeleton shows rather than
* a flash of "Not found". */
* that the app is not open to guests: a 403 on a guest app is a session for another
* app of the workspace, which a fresh sign-in replaces. True while discovery is
* pending, so the skeleton shows rather than a flash of "Not found". */
let offerSignIn = $derived(
status === 'noPermission' ||
(status === 'notExists' && deniedStatus === 403 && guestEntry !== 'none')
@@ -235,10 +234,9 @@
})
// The stale guest session must be gone before the card mounts: it still
// authenticates in this workspace, so the popup's success poll would see it and
// complete the sign-in before the new session ever lands. The card waits on
// `staleGuestCleared`, and a failed logout fails closed rather than offering a
// sign-in that could not complete.
// authenticates here, so the popup's success poll would see it and complete the
// sign-in before the new session lands. The card waits on `staleGuestCleared`; a
// failed logout fails closed rather than offering a sign-in that cannot complete.
let staleGuestCleared = $state(false)
let staleGuestLogoutFailed = $state(false)
$effect(() => {