mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-30 00:16:53 +00:00
* feat: let Slack agents answer through a workspace's own Slack app, with signing secrets set from the UI The instance Slack app's signing secret can be set in its OAuth setting as well as SLACK_SIGNING_SECRET, and a workspace's own Slack app takes its own in the workspace settings. Each secret only vouches for the workspaces connected through its app, so /windmill keeps working for a workspace whose app has none set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep Slack signing secrets out of logs and verify an own app without a secret with the instance's Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: report a workspace app's own signing secret in the Slack agents status Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: check Slack approval callbacks against the instance signing secret only An approval can be sent through any Slack resource, so the team's workspace connections do not tell which app signed its callback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: share the Slack app setup instructions and require the signing secret for agents Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: tidy the Slack signing secret copy and show a set secret as masked Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: check a workspace app only against its own signing secret, and approvals against their workspace's Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: accept a Slack approval signed by the workspace's app or the instance's Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: check Slack's signature on approvals only when sent through the workspace's Slack connection Any other Slack resource may belong to any Slack app, so its callbacks rest on the payload HMAC alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address review findings on Slack signature checks and the signing secret field - reject Slack signatures older than five minutes, so a captured request cannot be replayed - let a reachable workspace's command script take a message before another workspace's help reply - remove a saved workspace signing secret from its field: the button reads Remove when it is empty - set the signing secret on connect only when one was entered - tell non-admins who can set the signing secret Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: keep SLACK_SIGNING_SECRET for workspace apps without a secret, and route a verified request only to its workspaces - a workspace on its own app with no secret of its own is checked against SLACK_SIGNING_SECRET, which may hold that app's secret from before workspaces had one; the instance setting's secret still never applies to it - once a secret verifies a request, only the workspaces it verified take it, command scripts included: it proves which app sent the request - compute the replay window with abs_diff, which cannot overflow Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: update ee-repo-ref to c8d562aa9d75a7e99bf0ecd1ac224f532452a07e This commit updates the EE repository reference after PR #907 was merged in windmill-ee-private. Previous ee-repo-ref: 0864b0ab943defa8b8d55838ef09ab7dacfac17f New ee-repo-ref: c8d562aa9d75a7e99bf0ecd1ac224f532452a07e Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
306 lines
9.8 KiB
Svelte
306 lines
9.8 KiB
Svelte
<script lang="ts">
|
|
import { Plus, X } from 'lucide-svelte'
|
|
import CollapseLink from './CollapseLink.svelte'
|
|
import IconedResourceType from './IconedResourceType.svelte'
|
|
import Toggle from './Toggle.svelte'
|
|
import { untrack } from 'svelte'
|
|
import { enterpriseLicense } from '$lib/stores'
|
|
import Button from './common/button/Button.svelte'
|
|
import TextInput from './text_input/TextInput.svelte'
|
|
import Password from './Password.svelte'
|
|
import SettingCard from './instanceSettings/SettingCard.svelte'
|
|
import SlackAppSetup from './common/slack/SlackAppSetup.svelte'
|
|
|
|
interface Props {
|
|
name: string
|
|
value: any
|
|
login?: boolean
|
|
eeOnly?: boolean
|
|
}
|
|
|
|
let { name, value = $bindable(), login = true, eeOnly = false }: Props = $props()
|
|
|
|
// For Microsoft SSO, tenant is embedded in the login_config URLs, so we need
|
|
// a local state + bidirectional sync. For Teams, we bind directly to
|
|
// value['tenant'] in the template — no intermediate state, no effect loops.
|
|
let microsoftTenant: string = $state('')
|
|
|
|
// Sync microsoftTenant from value.login_config whenever value changes.
|
|
// Uses untrack for the write to avoid circular dependencies with the
|
|
// push effect below. Loop termination is guaranteed because
|
|
// URL.split('/')[3] of a URL we constructed with tenant X always yields X.
|
|
$effect(() => {
|
|
const v = value
|
|
untrack(() => {
|
|
try {
|
|
if (!v) {
|
|
if (microsoftTenant !== '') microsoftTenant = ''
|
|
return
|
|
}
|
|
if (
|
|
name == 'microsoft' &&
|
|
v['login_config'] &&
|
|
typeof v['login_config']['auth_url'] == 'string'
|
|
) {
|
|
const urlTenant = v['login_config']['auth_url'].split('/')[3]
|
|
if (urlTenant !== microsoftTenant) {
|
|
microsoftTenant = urlTenant
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Could not set tenantId', e)
|
|
}
|
|
})
|
|
})
|
|
|
|
// Push microsoftTenant edits back into value.login_config URLs.
|
|
$effect(() => {
|
|
microsoftTenant
|
|
if (name === 'microsoft') {
|
|
untrack(() => {
|
|
if (!value) return
|
|
if (microsoftTenant !== '') {
|
|
value = {
|
|
...value,
|
|
login_config: {
|
|
auth_url: `https://login.microsoftonline.com/${microsoftTenant}/oauth2/v2.0/authorize`,
|
|
token_url: `https://login.microsoftonline.com/${microsoftTenant}/oauth2/v2.0/token`,
|
|
userinfo_url: `https://graph.microsoft.com/oidc/userinfo`,
|
|
scopes: ['openid', 'profile', 'email']
|
|
}
|
|
}
|
|
} else {
|
|
if (value['login_config']) {
|
|
delete value['login_config']
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
let enabled = $derived(value != undefined && !(eeOnly && !$enterpriseLicense))
|
|
</script>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
<label
|
|
class="text-xs flex gap-4 items-center font-semibold text-emphasis {enabled ? 'rounded' : ''}"
|
|
><div class="w-[120px]"><IconedResourceType {name} after={true} /></div>
|
|
<Toggle
|
|
checked={enabled}
|
|
disabled={eeOnly && !$enterpriseLicense}
|
|
on:change={(e) => {
|
|
if (e.detail) {
|
|
if (name === 'ms_teams') {
|
|
value = { id: '', secret: '', tenant: '' }
|
|
} else {
|
|
value = { id: '', secret: '' }
|
|
}
|
|
} else {
|
|
value = undefined
|
|
}
|
|
}}
|
|
/>
|
|
{#if eeOnly && !$enterpriseLicense}
|
|
<div class="text-xs"> (EE only) </div>
|
|
{/if}
|
|
</label>
|
|
{#if enabled}
|
|
<SettingCard class="mb-4 flex flex-col gap-6">
|
|
{#if name != 'slack' && name != 'ms_teams'}
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-emphasis font-semibold text-xs">Custom Name</span>
|
|
<TextInput
|
|
inputProps={{ type: 'text', placeholder: 'Custom Name' }}
|
|
bind:value={value['display_name']}
|
|
/>
|
|
</label>
|
|
{/if}
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-emphasis font-semibold text-xs">Client Id</span>
|
|
<TextInput
|
|
inputProps={{ type: 'text', placeholder: 'Client Id' }}
|
|
bind:value={value['id']}
|
|
/>
|
|
</label>
|
|
<label for="{name}_client_secret" class="flex flex-col gap-1">
|
|
<span class="text-emphasis font-semibold text-xs">Client Secret</span>
|
|
<Password
|
|
id="{name}_client_secret"
|
|
placeholder="Client Secret"
|
|
bind:password={value['secret']}
|
|
/>
|
|
</label>
|
|
{#if name == 'slack'}
|
|
<label for="slack_signing_secret" class="flex flex-col gap-1">
|
|
<span class="text-emphasis font-semibold text-xs">Signing Secret</span>
|
|
<Password
|
|
id="slack_signing_secret"
|
|
placeholder="Signing Secret"
|
|
bind:password={value['signing_secret']}
|
|
/>
|
|
<span class="text-2xs text-hint">
|
|
Lets Windmill check that requests come from Slack. Required for AI agents; recommended
|
|
for <code>/windmill</code> commands.
|
|
</span>
|
|
</label>
|
|
{/if}
|
|
{#if name == 'microsoft' || name == 'ms_teams'}
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-emphasis font-semibold text-xs">Tenant Id</span>
|
|
{#if name === 'ms_teams'}
|
|
<TextInput
|
|
inputProps={{ type: 'text', placeholder: 'Tenant Id' }}
|
|
bind:value={value['tenant']}
|
|
/>
|
|
{:else}
|
|
<TextInput
|
|
inputProps={{ type: 'text', placeholder: 'Tenant Id' }}
|
|
bind:value={microsoftTenant}
|
|
/>
|
|
{/if}
|
|
</label>
|
|
{:else if login}
|
|
<label class="flex flex-col gap-1">
|
|
<span class="text-emphasis font-semibold text-xs">Allowed domains</span>
|
|
<div class="flex flex-col gap-1">
|
|
{#each value?.['allowed_domains'] ?? [] as domain, idx}
|
|
<div class="flex gap-2">
|
|
<input
|
|
class="max-w-96 w-full"
|
|
type="text"
|
|
bind:value={value['allowed_domains'][idx]}
|
|
onkeyup={(e) => {
|
|
if (domain == '') {
|
|
value['allowed_domains'] = value['allowed_domains']?.filter(
|
|
(d) => d != domain
|
|
)
|
|
}
|
|
}}
|
|
/>
|
|
<button
|
|
class="text-primary text-xs rounded hover:bg-surface-hover"
|
|
onclick={() => {
|
|
value['allowed_domains'] = value['allowed_domains']?.filter((d) => d != domain)
|
|
if (value['allowed_domains'].length == 0) {
|
|
value['allowed_domains'] = undefined
|
|
}
|
|
}}
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
<div class="flex gap-2">
|
|
<Button
|
|
variant="default"
|
|
unifiedSize="md"
|
|
startIcon={{ icon: Plus }}
|
|
onclick={() => {
|
|
value['allowed_domains'] = [...(value['allowed_domains'] ?? []), 'mydomain.com']
|
|
}}
|
|
>Add domain
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</label>
|
|
{/if}
|
|
{#if name == 'google'}
|
|
<CollapseLink text="Instructions">
|
|
<div class="text-xs text-primary rounded-md">
|
|
Create a new OAuth 2.0 Client <a
|
|
href="https://console.cloud.google.com/apis/credentials"
|
|
target="_blank">in Google console</a
|
|
>
|
|
and set the redirect URI to <code>BASE_URL/user/login_callback/google</code>
|
|
where BASE_URL is what you configured as core BASE_URL
|
|
</div>
|
|
</CollapseLink>
|
|
{:else if name == 'slack'}
|
|
<CollapseLink text="Set up slack">
|
|
<SlackAppSetup />
|
|
</CollapseLink>
|
|
{:else if name == 'microsoft'}
|
|
<CollapseLink text="Instructions">
|
|
<div class="text-xs text-primary border rounded-md p-4 space-y-3">
|
|
<div>
|
|
<strong>1. Create App Registration</strong>
|
|
<div class="ml-4 mt-1">
|
|
Create a new OAuth 2.0 Client <a
|
|
href="https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade"
|
|
target="_blank"
|
|
class="inline-flex items-center gap-1 whitespace-nowrap">in Microsoft portal</a
|
|
>:
|
|
<ul class="list-disc ml-4 mt-1 space-y-1">
|
|
<li>Click <strong>"Add"</strong> → <strong>"App Registration"</strong></li>
|
|
<li
|
|
>Select <strong
|
|
>"Accounts in this organizational directory only (Default Directory only -
|
|
Single tenant)"</strong
|
|
></li
|
|
>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>2. Authentication Configuration</strong>
|
|
<div class="ml-4 mt-1">
|
|
In the <strong>"Authentication"</strong> tab:
|
|
<ul class="list-disc ml-4 mt-1 space-y-1">
|
|
<li>Set the redirect URI to <strong>Web</strong></li>
|
|
<li
|
|
>Add redirect URI: <code class="bg-surface px-1 rounded text-xs"
|
|
>BASE_URL/user/login_callback/microsoft</code
|
|
></li
|
|
>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>3. Copy Credentials</strong>
|
|
<div class="ml-4 mt-1">
|
|
Copy the following values to Windmill:
|
|
<ul class="list-disc ml-4 mt-1 space-y-1">
|
|
<li>Copy <strong>"Directory (tenant ID)"</strong> to the tenant ID field</li>
|
|
<li>Copy <strong>"Application (client) ID"</strong> to the Client ID field</li>
|
|
<li
|
|
>Create a secret in <strong>"Client credentials"</strong> and copy to Client Secret
|
|
field</li
|
|
>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<strong>4. API Permissions</strong>
|
|
<div class="ml-4 mt-1">
|
|
Under <strong>"Delegated Permissions"</strong>, include:
|
|
<ul class="list-disc ml-4 mt-1 space-y-1">
|
|
<li>Sign in</li>
|
|
<li>Read user profile</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CollapseLink>
|
|
{:else if name == 'ms_teams'}
|
|
<CollapseLink text="Instructions">
|
|
<div class="text-xs text-primary rounded-md">
|
|
Follow this guide on <a
|
|
href="https://www.windmill.dev/docs/misc/setup_oauth#microsoft-teams"
|
|
target="_blank">Windmill Docs</a
|
|
>
|
|
to create a new Microsoft Teams App. Then paste Client ID, Tenant ID, and Client Secret here.
|
|
The same app serves the Teams bot (Enterprise Edition) and the Microsoft Teams OAuth resource:
|
|
to connect that resource, also add the redirect URI
|
|
<code>BASE_URL/oauth/callback/ms_teams</code> and the delegated Microsoft Graph permissions
|
|
you need to the app.
|
|
</div>
|
|
</CollapseLink>
|
|
{/if}
|
|
</SettingCard>
|
|
{/if}
|
|
</div>
|