mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 00:06:06 +00:00
feat(oauth): support per-provider sandbox URLs (#9358)
* feat(oauth): support per-provider sandbox URLs in registry + instance settings * fix(oauth): polish sandbox review nits (cc lookup, header label, ee ref) * refactor(oauth): drop dead build_oauth_clients duplicate in windmill-oauth * refactor(oauth): derive sandbox-capable provider list from registry * chore(docker): copy oauth_connect.json into frontend build stage * test(oauth): cover sandbox helpers (as_sandbox, canonical_name, resolve) * chore: update ee-repo-ref to 9297d8f790346e6a6ad540c7bca1a67f91ec11a2 This commit updates the EE repository reference after PR #595 was merged in windmill-ee-private. Previous ee-repo-ref: 3ab3eca9ac15ebab6db991e7964bc5e48ce21f42 New ee-repo-ref: 9297d8f790346e6a6ad540c7bca1a67f91ec11a2 Automated by sync-ee-ref workflow. --------- Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
windmill-internal-app[bot]
parent
889101b7f0
commit
2bf11dcb15
@@ -74,6 +74,16 @@
|
||||
let value: string = $state('')
|
||||
let valueToken: TokenResponse | undefined = undefined
|
||||
let connects: string[] | undefined = $state(undefined)
|
||||
|
||||
const SANDBOX_SUFFIX = '_sandbox'
|
||||
function stripSandboxSuffix(name: string): string {
|
||||
return name.endsWith(SANDBOX_SUFFIX) ? name.slice(0, -SANDBOX_SUFFIX.length) : name
|
||||
}
|
||||
// `resourceType` is always the canonical type (e.g. `docusign`) so resource
|
||||
// rows are uniform. `connectClient` carries the suffixed OAuth client name
|
||||
// (e.g. `docusign_sandbox`) used to look up credentials/URLs at runtime
|
||||
// and stored on `account.client` so token refresh hits the right endpoint.
|
||||
let connectClient: string = $state('')
|
||||
let connectsManual: { key: string; img?: string; instructions: string[] }[] | undefined =
|
||||
$state(undefined)
|
||||
let args: any = $state({})
|
||||
@@ -152,7 +162,9 @@
|
||||
description = ''
|
||||
labels = undefined
|
||||
wsSpecific = false
|
||||
resourceType = rt ?? ''
|
||||
const rawRt = rt ?? ''
|
||||
connectClient = rawRt
|
||||
resourceType = stripSandboxSuffix(rawRt)
|
||||
valueToken = undefined
|
||||
|
||||
// Reset client credentials state
|
||||
@@ -163,7 +175,7 @@
|
||||
tokenUrl = ''
|
||||
|
||||
await loadConnects()
|
||||
manual = !connects?.includes(resourceType)
|
||||
manual = !connects?.includes(connectClient)
|
||||
if (manual && express) {
|
||||
dispatch('error', 'Express OAuth setup is not available for non OAuth resource types')
|
||||
return
|
||||
@@ -312,7 +324,8 @@
|
||||
sendUserToast(data.error, true)
|
||||
step = 2
|
||||
} else if (data.type === 'success') {
|
||||
resourceType = data.resource_type
|
||||
connectClient = data.resource_type
|
||||
resourceType = stripSandboxSuffix(connectClient)
|
||||
value = data.res.access_token!
|
||||
valueToken = data.res
|
||||
responseExtra = data.extra ?? {}
|
||||
@@ -325,7 +338,7 @@
|
||||
}
|
||||
|
||||
async function getScopesAndParams() {
|
||||
const connect = await OauthService.getOauthConnect({ client: resourceType })
|
||||
const connect = await OauthService.getOauthConnect({ client: connectClient })
|
||||
scopes = connect.scopes ?? []
|
||||
extra_params = Object.entries(connect.extra_params ?? {}) as [string, string][]
|
||||
|
||||
@@ -401,7 +414,7 @@
|
||||
}
|
||||
|
||||
const tokenResponse = await OauthService.connectClientCredentials({
|
||||
client: resourceType,
|
||||
client: connectClient,
|
||||
requestBody
|
||||
})
|
||||
|
||||
@@ -428,7 +441,7 @@
|
||||
* Requires user interaction and consent
|
||||
* Opens popup for user to authenticate with OAuth provider
|
||||
*/
|
||||
const url = new URL(`/api/oauth/connect/${resourceType}`, window.location.origin)
|
||||
const url = new URL(`/api/oauth/connect/${connectClient}`, window.location.origin)
|
||||
url.searchParams.append('scopes', scopes.join('+'))
|
||||
if (extra_params.length > 0) {
|
||||
extra_params.forEach(([key, value]) => url.searchParams.append(key, value))
|
||||
@@ -490,7 +503,7 @@
|
||||
const accountData: any = {
|
||||
refresh_token: valueToken.refresh_token ?? '',
|
||||
expires_in: valueToken.expires_in,
|
||||
client: resourceType,
|
||||
client: connectClient,
|
||||
grant_type: valueToken.grant_type || 'authorization_code'
|
||||
}
|
||||
|
||||
@@ -602,6 +615,7 @@
|
||||
)
|
||||
step = 1
|
||||
resourceType = ''
|
||||
connectClient = ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,10 +674,11 @@
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
variant="default"
|
||||
selected={key === resourceType}
|
||||
selected={key === connectClient}
|
||||
on:click={() => {
|
||||
manual = false
|
||||
resourceType = key
|
||||
connectClient = key
|
||||
resourceType = stripSandboxSuffix(key)
|
||||
next()
|
||||
}}
|
||||
>
|
||||
@@ -703,6 +718,7 @@
|
||||
selected={key === resourceType}
|
||||
on:click={() => {
|
||||
manual = true
|
||||
connectClient = key
|
||||
resourceType = key
|
||||
next()
|
||||
}}
|
||||
@@ -725,6 +741,7 @@
|
||||
btnClasses={key === resourceType ? '!border-2' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
manual = true
|
||||
connectClient = key
|
||||
resourceType = key
|
||||
next()
|
||||
}}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import { tick } from 'svelte'
|
||||
import { Popover } from './meltComponents'
|
||||
import SettingsPageHeader from './settings/SettingsPageHeader.svelte'
|
||||
import oauthConnectRegistry from '$oauth_connect_registry'
|
||||
|
||||
interface Props {
|
||||
snowflakeAccountIdentifier?: string
|
||||
@@ -59,7 +60,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
const windmillBuiltins = [
|
||||
const windmillBuiltinsBase = [
|
||||
'azure_oauth',
|
||||
'github',
|
||||
'gitlab',
|
||||
@@ -82,7 +83,21 @@
|
||||
'teams',
|
||||
'zoho',
|
||||
'xero',
|
||||
'apify'
|
||||
'apify',
|
||||
'docusign'
|
||||
]
|
||||
// Providers whose registry entry (`backend/oauth_connect.json`) carries a
|
||||
// `sandbox` URL block. Each one gets a sibling `<name>_sandbox` dropdown
|
||||
// entry and is treated as a builtin so we don't render the custom-URL form
|
||||
// — the URLs come from the registry sandbox block. Derived at build time
|
||||
// from the registry so adding a sandbox to a provider needs no frontend
|
||||
// change.
|
||||
const windmillBuiltinsWithSandbox = Object.entries(oauthConnectRegistry)
|
||||
.filter(([, cfg]) => cfg && typeof cfg === 'object' && 'sandbox' in cfg)
|
||||
.map(([name]) => name)
|
||||
const windmillBuiltins = [
|
||||
...windmillBuiltinsBase,
|
||||
...windmillBuiltinsWithSandbox.map((n) => `${n}_sandbox`)
|
||||
]
|
||||
|
||||
let showCustomOAuthForm = $state(false)
|
||||
@@ -175,26 +190,29 @@
|
||||
}
|
||||
|
||||
function getOAuthProviderIcon(name: string) {
|
||||
// Sandbox variants share the parent provider's icon.
|
||||
const lookup = name.endsWith('_sandbox') ? name.slice(0, -'_sandbox'.length) : name
|
||||
|
||||
// Handle special cases
|
||||
if (name === 'teams') {
|
||||
if (lookup === 'teams') {
|
||||
return APP_TO_ICON_COMPONENT.ms_teams_webhook
|
||||
}
|
||||
if (name === 'snowflake_oauth') {
|
||||
if (lookup === 'snowflake_oauth') {
|
||||
return APP_TO_ICON_COMPONENT.snowflake
|
||||
}
|
||||
if (name === 'azure_oauth') {
|
||||
if (lookup === 'azure_oauth') {
|
||||
return APP_TO_ICON_COMPONENT.azure
|
||||
}
|
||||
|
||||
// Try direct mapping, fallback to Circle icon if not found
|
||||
return APP_TO_ICON_COMPONENT[name as keyof typeof APP_TO_ICON_COMPONENT] || Circle
|
||||
return APP_TO_ICON_COMPONENT[lookup as keyof typeof APP_TO_ICON_COMPONENT] || Circle
|
||||
}
|
||||
|
||||
function generateOAuthDropdownItems(): Item[] {
|
||||
const items: Item[] = []
|
||||
|
||||
// Add built-in providers that are not already configured
|
||||
windmillBuiltins.forEach((name) => {
|
||||
windmillBuiltinsBase.forEach((name) => {
|
||||
// Only show providers that are not already in the oauths object
|
||||
if (!oauths || !oauths[name]) {
|
||||
const icon = getOAuthProviderIcon(name)
|
||||
@@ -206,6 +224,19 @@
|
||||
}
|
||||
})
|
||||
|
||||
// Add sandbox variants for providers that have sandbox URLs in the registry
|
||||
windmillBuiltinsWithSandbox.forEach((name) => {
|
||||
const sandboxKey = `${name}_sandbox`
|
||||
if (!oauths || !oauths[sandboxKey]) {
|
||||
const icon = getOAuthProviderIcon(name)
|
||||
items.push({
|
||||
displayName: `${capitalize(name)} (sandbox)`,
|
||||
action: () => createOAuthClient(sandboxKey),
|
||||
icon: icon
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Add custom option
|
||||
items.push({
|
||||
displayName: `Custom OAuth client ${!$enterpriseLicense ? '(requires ee)' : ''}`,
|
||||
@@ -370,11 +401,14 @@
|
||||
{#if oauths[k] && !(oauths[k] && 'login_config' in oauths[k])}
|
||||
{#if !['slack', 'teams'].includes(k) && oauths[k]}
|
||||
{@const IconComponent = getOAuthProviderIcon(k) as any}
|
||||
{@const headerLabel = k.endsWith('_sandbox')
|
||||
? `${k.slice(0, -'_sandbox'.length)} (sandbox)`
|
||||
: k}
|
||||
<div class="flex flex-col gap-2 pb-6">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<IconComponent size={24} width="24" height="24" class="shrink-0" />
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="text-xs font-semibold text-emphasis">{k}</label>
|
||||
<label class="text-xs font-semibold text-emphasis">{headerLabel}</label>
|
||||
<Button
|
||||
variant="subtle"
|
||||
destructive
|
||||
|
||||
Reference in New Issue
Block a user