mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 08:03:50 +00:00
fix(frontend): treat an unread hub setting as private, not as the public hub
`hubBaseUrlStore` is seeded with the public hub and written in one place, by a loader with no catch and no retry. A settings read that threw therefore left the store naming hub.windmill.dev for the rest of the session, and the import counter read that as permission to report a private instance's project slug — the leak the previous commit closed, narrowed to "after one failed read". The fact has three states and the store held two, so `hubBaseUrlKnown` carries the third: the loader sets it only once the value is the instance's own, and the telemetry key requires it. Links keep rendering the default meanwhile, which is what they always did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JirHCYVR6qg7Xqe4PcZ1KG
This commit is contained in:
co-authored by
Claude Opus 5
parent
36352c482a
commit
63b8d9757b
@@ -118,6 +118,10 @@ export const superadmin = writable<string | false | undefined>(undefined)
|
||||
export const devopsRole = writable<string | false | undefined>(undefined)
|
||||
export const lspTokenStore = writable<string | undefined>(undefined)
|
||||
export const hubBaseUrlStore = writable<string>(DEFAULT_HUB_BASE_URL)
|
||||
// Whether the store above is the instance's answer or still the default it was seeded with.
|
||||
// It reads as the public hub either way, which is fine for a link and wrong for anything
|
||||
// deciding what may be reported about a hub — those must treat unknown as private.
|
||||
export const hubBaseUrlKnown = writable<boolean>(false)
|
||||
export const wsBaseUrlStore = writable<string | undefined>(undefined)
|
||||
export const disableHubStore = writable<boolean>(false)
|
||||
// What a superadmin standing in a workspace they are not a member of needs to see it as a
|
||||
|
||||
@@ -2,23 +2,28 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('$lib/gen', () => ({ OpenAPI: { BASE: '/api' } }))
|
||||
|
||||
// A store `get()` can read, so a test can say which hub the instance points at.
|
||||
// Stores `get()` can read, so a test can say which hub the instance points at and whether
|
||||
// the instance has answered at all.
|
||||
const hubBaseUrl = vi.hoisted(() => {
|
||||
let value = 'https://hub.windmill.dev'
|
||||
return {
|
||||
set: (v: string) => (value = v),
|
||||
store: {
|
||||
subscribe: (run: (v: string) => void) => {
|
||||
run(value)
|
||||
return () => {}
|
||||
const readable = <T>(initial: T) => {
|
||||
let value = initial
|
||||
return {
|
||||
set: (v: T) => (value = v),
|
||||
store: {
|
||||
subscribe: (run: (v: T) => void) => {
|
||||
run(value)
|
||||
return () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { url: readable('https://hub.windmill.dev'), known: readable(true) }
|
||||
})
|
||||
|
||||
vi.mock('$lib/stores', () => ({
|
||||
workspaceStore: { subscribe: () => () => {} },
|
||||
hubBaseUrlStore: hubBaseUrl.store
|
||||
hubBaseUrlStore: hubBaseUrl.url.store,
|
||||
hubBaseUrlKnown: hubBaseUrl.known.store
|
||||
}))
|
||||
|
||||
import {
|
||||
@@ -137,11 +142,21 @@ describe('hubProjectUsageKey', () => {
|
||||
'https://hub.windmill.dev:443',
|
||||
' https://hub.windmill.dev '
|
||||
]) {
|
||||
hubBaseUrl.set(hub)
|
||||
hubBaseUrl.url.set(hub)
|
||||
expect(hubProjectUsageKey('stripe-invoices'), hub).toBe('stripe-invoices')
|
||||
}
|
||||
})
|
||||
|
||||
it('answers private until the instance has said which hub it points at', () => {
|
||||
// The store is seeded with the public hub, so a settings read that failed must not
|
||||
// read as permission to report the name.
|
||||
hubBaseUrl.url.set('https://hub.windmill.dev')
|
||||
hubBaseUrl.known.set(false)
|
||||
expect(hubProjectUsageKey('acme-payroll')).toBe('private')
|
||||
hubBaseUrl.known.set(true)
|
||||
expect(hubProjectUsageKey('acme-payroll')).toBe('acme-payroll')
|
||||
})
|
||||
|
||||
it("keeps a private hub's project names off the wire", () => {
|
||||
// The slug is the customer's own content on an instance running its own hub, and the
|
||||
// disclosure only claims public project names.
|
||||
@@ -152,7 +167,7 @@ describe('hubProjectUsageKey', () => {
|
||||
'hub.windmill.dev',
|
||||
'not a url'
|
||||
]) {
|
||||
hubBaseUrl.set(hub)
|
||||
hubBaseUrl.url.set(hub)
|
||||
expect(hubProjectUsageKey('acme-payroll'), hub).toBe('private')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { get } from 'svelte/store'
|
||||
import { OpenAPI } from '$lib/gen'
|
||||
import { hubBaseUrlStore, workspaceStore } from '$lib/stores'
|
||||
import { hubBaseUrlKnown, hubBaseUrlStore, workspaceStore } from '$lib/stores'
|
||||
import { DEFAULT_HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION } from '$lib/hub'
|
||||
|
||||
// Anonymous product-usage counters (e.g. AI session activity), batched into the
|
||||
@@ -195,10 +195,13 @@ export function hubScriptUsageKey(script: {
|
||||
* and what the disclosure means by "the name of any public hub project".
|
||||
*
|
||||
* Compared by host, so the port, scheme and trailing slash an operator may have typed do
|
||||
* not decide it. Anything unparseable answers private.
|
||||
* not decide it. Anything unparseable, and anything not yet read, answers private.
|
||||
*/
|
||||
export function hubProjectUsageKey(slug: string): string {
|
||||
return isPublicHub(get(hubBaseUrlStore)) ? slug : PRIVATE_HUB_KEY
|
||||
// `hubBaseUrlKnown` and not the URL alone: the store is seeded with the public hub, so an
|
||||
// instance whose setting could not be read would otherwise report its own project names.
|
||||
if (!get(hubBaseUrlKnown) || !isPublicHub(get(hubBaseUrlStore))) return PRIVATE_HUB_KEY
|
||||
return slug.slice(0, 100)
|
||||
}
|
||||
|
||||
function isPublicHub(hub: string): boolean {
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
type UserExt,
|
||||
defaultScripts,
|
||||
hubBaseUrlStore,
|
||||
hubBaseUrlKnown,
|
||||
wsBaseUrlStore,
|
||||
disableHubStore,
|
||||
usedTriggerKinds,
|
||||
@@ -474,10 +475,18 @@
|
||||
}
|
||||
|
||||
async function loadHubBaseUrl() {
|
||||
$hubBaseUrlStore =
|
||||
((await SettingService.getGlobal({ key: 'hub_accessible_url' })) as string) ||
|
||||
((await SettingService.getGlobal({ key: 'hub_base_url' })) as string) ||
|
||||
DEFAULT_HUB_BASE_URL
|
||||
// A read that throws leaves the store on its seeded default, which names the public hub
|
||||
// — so the flag, not the value, is what says the instance has answered. An instance that
|
||||
// simply has no setting still answers: the chain falls through to the default.
|
||||
try {
|
||||
$hubBaseUrlStore =
|
||||
((await SettingService.getGlobal({ key: 'hub_accessible_url' })) as string) ||
|
||||
((await SettingService.getGlobal({ key: 'hub_base_url' })) as string) ||
|
||||
DEFAULT_HUB_BASE_URL
|
||||
$hubBaseUrlKnown = true
|
||||
} catch (error) {
|
||||
console.error('Could not read the hub URL:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWsBaseUrl() {
|
||||
|
||||
Reference in New Issue
Block a user