Hide session list for anonymous UDS users.

Filter session.list/search on the client when cookies are absent, ignore broadcast session-added events, and deepen host listState wrapping so ungrouped rows no longer leak while logged out.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-08 07:53:35 +08:00
parent 92ee170b2e
commit db907a9489
2 changed files with 107 additions and 0 deletions

View file

@ -464,9 +464,11 @@ window.__ModuleLoader__.load({
const perms = user?.permissions || {}
const canSettings = user ? !!perms.canAccessSettings : false
const canCreateWs = user ? !!perms.canCreateWorkspace : false
document.documentElement.setAttribute('data-uds-logged-in', user ? '1' : '0')
document.documentElement.setAttribute('data-uds-can-settings', canSettings ? '1' : '0')
document.documentElement.setAttribute('data-uds-can-create-ws', canCreateWs ? '1' : '0')
return () => {
document.documentElement.removeAttribute('data-uds-logged-in')
document.documentElement.removeAttribute('data-uds-can-settings')
document.documentElement.removeAttribute('data-uds-can-create-ws')
}
@ -483,6 +485,7 @@ window.__ModuleLoader__.load({
const me = await fetchJson('/uds-auth/api/me')
if (!me?.authenticated || !me?.data) {
setUser(null)
window.dispatchEvent(new Event('uds-auth-changed'))
return
}
const d = me.data
@ -495,8 +498,10 @@ window.__ModuleLoader__.load({
role: d.role || 'user',
permissions: d.permissions || {},
})
window.dispatchEvent(new Event('uds-auth-changed'))
} catch {
setUser(null)
window.dispatchEvent(new Event('uds-auth-changed'))
} finally {
setLoading(false)
}
@ -794,6 +799,71 @@ window.__ModuleLoader__.load({
}
}, 'uds-auth: settings-gate')
// Defense in depth: never show Host session rows while UDS cookies are absent.
// Covers ALS misses on /api and broadcast api-session/added leaks.
// Use ctx.get (optional) — sandboxed clients cannot ctx.inject undeclared services.
ctx.effect(() => {
let onAuthChanged = null
let installedRpc = false
let installedSessions = false
const install = () => {
const connection = ctx.get('connection')
const rpc = connection && connection.rpc
if (!installedRpc && rpc && typeof rpc.call === 'function' && !rpc.__udsAuthListGate) {
installedRpc = true
rpc.__udsAuthListGate = true
const origCall = rpc.call.bind(rpc)
rpc.call = async function udsAuthRpcCall(channel, endpoint, payload, signal) {
const result = await origCall(channel, endpoint, payload, signal)
if (getEmpNo()) return result
if (channel !== '/api') return result
if (endpoint === 'session/list') return { ok: true, value: { items: [] } }
if (endpoint === 'session/search') return { ok: true, value: { items: [], hasMore: false } }
return result
}
}
const sessions = ctx.get('sessions')
if (!installedSessions && sessions && !sessions.__udsAuthListGate) {
installedSessions = true
sessions.__udsAuthListGate = true
const origAdded = sessions.handleSessionAdded && sessions.handleSessionAdded.bind(sessions)
if (origAdded) {
sessions.handleSessionAdded = (summary) => {
if (!getEmpNo()) return
return origAdded(summary)
}
}
const origActivity = sessions.handleSessionActivity && sessions.handleSessionActivity.bind(sessions)
if (origActivity) {
sessions.handleSessionActivity = (sessionId, updatedAt) => {
if (!getEmpNo()) return
return origActivity(sessionId, updatedAt)
}
}
const refresh = () => {
if (typeof sessions.refresh === 'function') void sessions.refresh()
}
refresh()
onAuthChanged = refresh
window.addEventListener('uds-auth-changed', onAuthChanged)
}
return installedRpc && installedSessions
}
install()
const timer = installedRpc && installedSessions ? null : setInterval(() => {
if (install() && timer) clearInterval(timer)
}, 300)
return () => {
if (timer) clearInterval(timer)
if (onAuthChanged) window.removeEventListener('uds-auth-changed', onAuthChanged)
}
}, 'uds-auth: session-list-gate')
// sidebar.footer.action; layout effect lays footArea as one row: Settings | UDS login
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
name: 'sidebar.footer.action',

View file

@ -102,6 +102,17 @@ export function patchWebServerWithIdentity(server, resolveIdentity) {
server.registerFallback = (handler) => origFallback(wrap(handler))
}
const origRegisterUpgrade = server.registerUpgrade?.bind(server)
if (origRegisterUpgrade) {
server.registerUpgrade = (route) => origRegisterUpgrade({
...route,
handler: async (req, socket, head) => {
const identity = await resolveIdentity(req)
return withUserContext(identity, () => route.handler(req, socket, head))
},
})
}
return () => {
/* leave patched — reload recreates webServer fiber */
}
@ -150,6 +161,32 @@ export function installDshAcl(ctx, {
if (!sc || sc.__udsAcl) return
sc.__udsAcl = true
// Deeper wrap: ApiSessionList.list (cold summaries)
if (sc.listState && typeof sc.listState.list === 'function' && !sc.listState.__udsAcl) {
sc.listState.__udsAcl = true
const origStateList = sc.listState.list.bind(sc.listState)
sc.listState.list = async (signal) => {
const items = await origStateList(signal)
const identity = getUserContext()
if (!identity?.empNo) return []
if (identity.permissions?.canViewAllSessions) return items
return (items || []).filter((row) => {
const id = row?.sessionId ?? row?.id
return id != null && sessionAcl.canViewSession(id)
})
}
if (typeof sc.listState.search === 'function') {
const origStateSearch = sc.listState.search.bind(sc.listState)
sc.listState.search = async (query, signal) => {
const value = await origStateSearch(query, signal)
const identity = getUserContext()
if (!identity?.empNo) return { items: [], hasMore: false }
if (identity.permissions?.canViewAllSessions) return value
return sessionAcl.filterListValue(value)
}
}
}
const origList = sc.list.bind(sc)
sc.list = async (request, signal) => {
const value = await origList(request, signal)