mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-29 00:03:09 +00:00
fix: treat the acting user as unknown during a workspace switch (#12049)
* fix: treat the acting user as unknown during a workspace switch * docs: align acting-user comments with the workspace switch check
This commit is contained in:
@@ -8,10 +8,10 @@ import { getWorkspaceRole, type RoleLookup } from '$lib/user'
|
||||
* The user acting in a workspace that is not necessarily the one the top nav points at — an AI
|
||||
* session or a workspace-specific variant acts on a workspace the nav deliberately is not on.
|
||||
*
|
||||
* `$userStore` answers for the navigation workspace at no cost, exactly as every permission check
|
||||
* in the app did before this hook existed — including when it holds nobody, which reads as unknown
|
||||
* and refuses. Every other workspace is looked up, and an unresolved user there is `undefined`: it
|
||||
* must never fall back to the navigation user, whose rights belong to another workspace.
|
||||
* `$userStore` answers for the navigation workspace at no cost once it was fetched for it —
|
||||
* including when it holds nobody, which reads as unknown and refuses. Every other workspace is
|
||||
* looked up, and an unresolved user there is `undefined`: it must never fall back to the
|
||||
* navigation user, whose rights belong to another workspace.
|
||||
* `canWrite`/`isOwner` refuse for an unknown user, which is the only safe answer. A caller that
|
||||
* must not render that refusal as a denial asks `resolved` first.
|
||||
*/
|
||||
@@ -43,9 +43,13 @@ export function useActingUser(workspace: () => string | undefined) {
|
||||
})
|
||||
})
|
||||
|
||||
// Right after a switch, `$userStore` still holds the previous workspace's membership until
|
||||
// the new `whoami` lands: that is unknown, not an answer.
|
||||
const navStale = () => !!navUser.current && navUser.current.workspace_id !== navWorkspace.current
|
||||
|
||||
function userIn(ws: string | undefined): UserExt | undefined {
|
||||
if (!ws) return undefined
|
||||
if (ws === navWorkspace.current) return navUser.current
|
||||
if (ws === navWorkspace.current) return navStale() ? undefined : navUser.current
|
||||
const lookup = looked.get(ws)
|
||||
return lookup?.kind === 'resolved' ? lookup.user : undefined
|
||||
}
|
||||
@@ -55,9 +59,12 @@ export function useActingUser(workspace: () => string | undefined) {
|
||||
* hook has been pointed at are looked up; the rest read as unknown. */
|
||||
in: userIn,
|
||||
/** Whether `ws` has an answer at all — a user, or a lookup that came back without one.
|
||||
* The navigation workspace always has one: `$userStore`, "nobody" included. */
|
||||
* The navigation workspace has one once `$userStore` was fetched for it, "nobody"
|
||||
* included. */
|
||||
resolved: (ws: string | undefined): boolean =>
|
||||
!!ws && (ws === navWorkspace.current || looked.has(ws)),
|
||||
!!ws && (ws === navWorkspace.current ? !navStale() : looked.has(ws)),
|
||||
/** Unknown right after a workspace switch: load lists without waiting for it, as it only
|
||||
* gates their controls, or they keep the previous workspace's rows. */
|
||||
get current(): UserExt | undefined {
|
||||
return userIn(workspace())
|
||||
},
|
||||
|
||||
@@ -1902,15 +1902,9 @@ addEventListener('message', function (e) {
|
||||
function previewCtx() {
|
||||
return $state.snapshot({ ctx: operatingUser.current, workspace: opWorkspace, path })
|
||||
}
|
||||
|
||||
// A build fed before the viewer is known keeps that viewer until the next one. Outside
|
||||
// the navigation workspace the viewer is looked up; in it, `$userStore` still holds the
|
||||
// previous workspace's membership right after a switch. A failed lookup also settles
|
||||
// this: the app then runs with no viewer, as we don't know who it is.
|
||||
const previewUserPending = $derived(
|
||||
!!opWorkspace &&
|
||||
(!operatingUser.resolved(opWorkspace) ||
|
||||
(!!operatingUser.current && operatingUser.current.workspace_id !== opWorkspace))
|
||||
|
||||
// A build fed before the viewer is known keeps that viewer until the next one. A failed
|
||||
// lookup also settles this: the app then runs with no viewer, as we don't know who it is.
|
||||
const previewUserPending = $derived(!!opWorkspace && !operatingUser.resolved(opWorkspace))
|
||||
$effect(() => {
|
||||
if (previewUserPending) return
|
||||
|
||||
@@ -203,8 +203,8 @@
|
||||
)
|
||||
let filters = useUrlSyncedFilterInstance(untrack(() => resourcesFilterSchema))
|
||||
// A link can carry the user folders filter to someone it is not offered to, where it would sit
|
||||
// set with no chip to clear it. Dropped only once a user is actually known: `resolved` holds for
|
||||
// the navigation workspace even while its user is still loading.
|
||||
// set with no chip to clear it. Dropped only once a user is actually known: `resolved` also
|
||||
// holds for a lookup that came back with nobody.
|
||||
$effect(() => {
|
||||
if (!operatingUser.resolved($operatingWorkspace) || !operatingUser.current) return
|
||||
if (userFoldersFilterType !== undefined) return
|
||||
|
||||
@@ -280,8 +280,8 @@
|
||||
)
|
||||
let filters = useUrlSyncedFilterInstance(untrack(() => schedulesFilterSchema))
|
||||
// A link can carry the user folders filter to someone it is not offered to, where it would sit
|
||||
// set with no chip to clear it. Dropped only once a user is actually known: `resolved` holds for
|
||||
// the navigation workspace even while its user is still loading.
|
||||
// set with no chip to clear it. Dropped only once a user is actually known: `resolved` also
|
||||
// holds for a lookup that came back with nobody.
|
||||
$effect(() => {
|
||||
if (!operatingUser.resolved($operatingWorkspace) || !operatingUser.current) return
|
||||
if (userFoldersFilterType !== undefined) return
|
||||
|
||||
@@ -106,8 +106,8 @@
|
||||
)
|
||||
let filters = useUrlSyncedFilterInstance(untrack(() => variablesFilterSchema))
|
||||
// A link can carry the user folders filter to someone it is not offered to, where it would sit
|
||||
// set with no chip to clear it. Dropped only once a user is actually known: `resolved` holds for
|
||||
// the navigation workspace even while its user is still loading.
|
||||
// set with no chip to clear it. Dropped only once a user is actually known: `resolved` also
|
||||
// holds for a lookup that came back with nobody.
|
||||
$effect(() => {
|
||||
if (!operatingUser.resolved($operatingWorkspace) || !operatingUser.current) return
|
||||
if (userFoldersFilterType !== undefined) return
|
||||
|
||||
Reference in New Issue
Block a user