mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
d6c642b170
* feat: add Azure Event Grid triggers (EE)
Introduces a new enterprise trigger kind `azure` that supports three
modes via a single unified trigger type:
- basic_push: Azure Event Grid basic — custom topics, system topics
(Storage, Resource Manager, Key Vault, etc.), domains (push only)
- namespace_push: Event Grid Namespace topics (CloudEvents over HTTP push)
- namespace_pull: Event Grid Namespace topics (HTTP pull with lock-token
ack/reject for dead-lettering)
Auth uses a Service Principal resource (tenant_id, client_id,
client_secret, subscription_id). Subscriptions are created in
CloudEvents 1.0 schema so the push webhook handler and the pull listener
share one payload parser.
Backend
- New crate `windmill-trigger-azure` (OSS stubs + EE impl symlinked from
windmill-ee-private)
- Migration `azure_trigger` table with CHECK constraints enforcing
mode/columns coherence
- `TriggerKind::Azure`, `JobTriggerKind::Azure`,
`DeployedObject::AzureTrigger` variants
- Push route `/api/azure/w/{workspace}/*path` handles classic
Event Grid SubscriptionValidation handshake and CloudEvents 1.0
abuse-protection OPTIONS handshake
- Optional inbound JWT validation (audience check only for v1)
- Feature flag `azure_trigger` propagated through windmill-api,
windmill-store (resource helper), and added to ee_core
Frontend
- `triggers/azure/` editor with mode toggle (basic/namespace-push/
namespace-pull) and per-mode config (topic ARM id / namespace +
topic name / subscription / filters / push auth / pull options)
- Registered in icon map, display names, save functions, badge,
wrapper, editor, add-trigger menu
OpenAPI
- `AzureTrigger`, `AzureTriggerData`, `AzureMode`,
`AzureSubscriptionMode`, `AzureDeliveryConfig`, `TestAzureConnection`
schemas; `/azure_triggers/*` endpoints; client regenerated
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to eaa7c3a9cb37a9ccc93f10a2535d929365acd2d8
This commit updates the EE repository reference after PR #541 was merged in windmill-ee-private.
Previous ee-repo-ref: 9689014e8c12c36c1059fd8fa5758d550b8b8bc9
New ee-repo-ref: eaa7c3a9cb37a9ccc93f10a2535d929365acd2d8
Automated by sync-ee-ref workflow.
* feat(azure-trigger): secret-auth push, ARM discovery, capture isolation, CLI + parity
Frontend:
- Split mode selector into Namespace/Basic + Pull/Push
- ARM resource dropdowns (namespaces, Basic topics, namespace topics)
populated from the service principal; cascade with stale-selection
reset on SP / edition change
- Remove stale authenticate toggle + audience input (server-managed
push_auth_config has replaced them)
- Azure listing page: "Create from template" button; "Also delete Azure
subscription" toggle in the delete modal; simplified trigger label
falling back to path
- AzureCapture.svelte: "Test subscription name" with -wm-capture suffix
- CompareWorkspaces.svelte: wire Azure for fork/compare
- Drop Trigger-deployed/event-loss warning (capture subscription is
isolated with -wm-capture)
Backend:
- Shared-secret push auth (see EE crate for detail)
- JSONB push_auth_config column (renamed from delivery_config), #[serde(skip)]
so clients/CLI/exports never see it
- Drop redundant enabled column; mode supersedes
- Azure capture infra: AzureTriggerConfig + set_azure_trigger_config +
azure_payload route + TriggerKind::Azure arm; PT15M queue TTL on
capture subscriptions so they bound storage after tab close
- Granular ACLs, users offboarding, trash, git-sync deployed-object:
all include azure_trigger
CLI:
- Add azure to TRIGGER_TYPES, pushObj dispatch, getTypeStrFromPath,
trigger commands (get/update/create/list/template), sync delete
switch + regex; e2e test for `trigger new --kind azure`
- system_prompts: SCHEMA_MAPPINGS + schema_names include AzureTrigger;
auto-generated/* regenerated
Skill:
- .claude/skills/adding-a-trigger/ checklist covering every file that
needs editing when wiring a new trigger type (learned from this PR)
ee-repo-ref bumped to b0e490cbf3724b7b64c6a5b010e3bdf24acd873c.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(azure-trigger): ci — ShareModal Kind + regenerated system_prompts
- frontend/src/lib/components/ShareModal.svelte: add 'azure_trigger'
to the Kind type so the listing page's "Permissions" action compiles
(ts2345 — caught by npm_check on CI, missed by fast-check locally).
- system_prompts/auto-generated/: regenerate to drop the stale
delivery_config / AzureDeliveryConfig fields from the Azure schema
(check-freshness on CI).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(azure-trigger): use workspace constant_time_eq crate
Drop hand-rolled constant-time compare in favour of the workspace
constant_time_eq crate (same one used by http_trigger_auth).
ee-repo-ref bumped to 9659382d47286e7f7f66d01b6f5dd8d4ed34848b.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(azure-trigger): pass placeholder + disabled via inputProps
`TextInput`'s `placeholder` and `disabled` go through its `inputProps`
prop — CI's `npm run check` caught the stale top-level passing that
`npm run check:fast` missed. Align with the DefaultEmailConfigSection
pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(azure-trigger): correct LATEST_GIT_SYNC_SCRIPT_PATH version to 28213
The hub deploy of the azure-aware sync-script is version 28213, not
28214. Backend was pinning a non-existent hub script, which broke the
git_sync_e2e suite (every deploy's sync step 404'd).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(azure-trigger): add azure_triggers to token scope selector + skill
- windmill-api/src/token.rs: `build_trigger_scope_domains` was missing
`("azure_triggers", "Azure Event Grid")`, so the CreateToken UI's scope
selector didn't surface azure_triggers:read/write. Backend already had
`ScopeDomain::AzureTriggers` wired (scopes.rs), this just exposes it.
- .claude/skills/adding-a-trigger/SKILL.md: capture both scope-related
files under the hardcoded-arrays section so future triggers don't miss
the UI surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(adding-a-trigger-skill): clarify token.rs scope effect
Not a regression — nothing was working before. Skipping TRIGGER_DOMAINS
just means the scope works via API/CLI but has no UI checkbox.
* docs(adding-a-trigger-skill): trim token.rs bullet
* fix(azure-trigger): regen openapi-deref + swap textarea for TextInput
- Run build_openapi.sh to regenerate openapi-deref.{yaml,json} with the
12 azure_triggers paths + schemas. These files are served by the
runtime (include_str! in windmill-api/src/lib.rs) to external SDK
consumers; without this regen the new endpoints wouldn't be advertised.
- Replace the raw <textarea> for event type filters with the
design-system TextInput in textarea mode (frontend/CLAUDE.md bans raw
HTML elements).
Addresses cubic + claude PR review items.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
210 lines
6.6 KiB
Rust
210 lines
6.6 KiB
Rust
use axum::{routing::get, Json, Router};
|
|
use lazy_static::lazy_static;
|
|
use serde::{Deserialize, Serialize};
|
|
use windmill_common::error::JsonResult;
|
|
|
|
#[derive(Default, Serialize, Deserialize, Clone)]
|
|
pub struct ScopeOption {
|
|
pub value: String,
|
|
pub label: String,
|
|
pub requires_resource_path: bool,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
pub struct ScopeDomain {
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub scopes: Vec<ScopeOption>,
|
|
}
|
|
|
|
fn build_trigger_scope_domains() -> Vec<ScopeDomain> {
|
|
const TRIGGER_DOMAINS: &[(&str, &str)] = &[
|
|
("http_triggers", "HTTP"),
|
|
("websocket_triggers", "WebSocket"),
|
|
("kafka_triggers", "Kafka"),
|
|
("nats_triggers", "NATS"),
|
|
("mqtt_triggers", "MQTT"),
|
|
("sqs_triggers", "AWS SQS"),
|
|
("gcp_triggers", "GCP Pub/Sub"),
|
|
("azure_triggers", "Azure Event Grid"),
|
|
("postgres_triggers", "PostgreSQL"),
|
|
("email_triggers", "Email"),
|
|
];
|
|
|
|
TRIGGER_DOMAINS
|
|
.iter()
|
|
.map(|(domain, display_name)| ScopeDomain {
|
|
name: format!("{} Triggers", display_name),
|
|
description: Some(format!("{} trigger management", display_name)),
|
|
scopes: vec![
|
|
ScopeOption {
|
|
value: format!("{domain}:read"),
|
|
label: "Read".to_string(),
|
|
requires_resource_path: true,
|
|
},
|
|
ScopeOption {
|
|
value: format!("{domain}:write"),
|
|
label: "Write".to_string(),
|
|
requires_resource_path: true,
|
|
},
|
|
],
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn build_standard_scope_domains() -> Vec<ScopeDomain> {
|
|
const STANDARD_DOMAINS: &[(&str, &str, &str, bool)] = &[
|
|
(
|
|
"scripts",
|
|
"Scripts",
|
|
"Access to automation scripts and workflows",
|
|
true,
|
|
),
|
|
(
|
|
"flows",
|
|
"Flows",
|
|
"Access to automation scripts and workflows",
|
|
true,
|
|
),
|
|
(
|
|
"flow_conversations",
|
|
"Flow Conversations",
|
|
"Flow conversation management",
|
|
false,
|
|
),
|
|
("apps", "Apps", "App management", true),
|
|
("raw_apps", "RawApps", "Raw app management", true),
|
|
("resources", "Resources", "Resource management", true),
|
|
("variables", "Variables", "", true),
|
|
(
|
|
"schedules",
|
|
"Schedules",
|
|
"Scheduled tasks and automated triggers",
|
|
true,
|
|
),
|
|
("folders", "Folders", "Folder management", true),
|
|
("users", "Users", "User account management", false),
|
|
("groups", "Groups", "Group management", false),
|
|
("workspaces", "Workspaces", "Workspace management", false),
|
|
("audit", "Audit", "Audit log management", false),
|
|
("workers", "Workers", "Worker management", false),
|
|
("settings", "Settings", "System settings management", false),
|
|
(
|
|
"service_logs",
|
|
"Service Logs",
|
|
"Service log management",
|
|
false,
|
|
),
|
|
("configs", "Configs", "Configuration management", false),
|
|
("oauth", "OAuth", "OAuth management", false),
|
|
("ai", "AI", "AI feature management", false),
|
|
(
|
|
"agent_workers",
|
|
"Agent Workers",
|
|
"Agent worker management",
|
|
false,
|
|
),
|
|
("drafts", "Drafts", "Draft management", false),
|
|
("favorites", "Favorites", "Favorite items management", false),
|
|
("inputs", "Inputs", "Input management", false),
|
|
("job_helpers", "Job Helpers", "Job helper utilities", false),
|
|
(
|
|
"openapi",
|
|
"OpenAPI",
|
|
"OpenAPI documentation management",
|
|
false,
|
|
),
|
|
("capture", "Capture", "Request capture management", false),
|
|
(
|
|
"concurrency_groups",
|
|
"Concurrency Groups",
|
|
"Concurrency group management",
|
|
false,
|
|
),
|
|
("oidc", "OIDC", "OIDC management", false),
|
|
("acls", "ACLs", "Access Control List management", false),
|
|
("indexer", "Indexer", "Search indexer management", false),
|
|
("teams", "Teams", "Team management", false),
|
|
(
|
|
"git_sync",
|
|
"Git Sync",
|
|
"Git synchronization management",
|
|
false,
|
|
),
|
|
(
|
|
"native_triggers",
|
|
"Native Triggers",
|
|
"Native triggers management",
|
|
true,
|
|
),
|
|
];
|
|
|
|
STANDARD_DOMAINS
|
|
.iter()
|
|
.map(|(key, name, desc, req)| ScopeDomain {
|
|
name: name.to_string(),
|
|
description: if desc.is_empty() {
|
|
None
|
|
} else {
|
|
Some(desc.to_string())
|
|
},
|
|
scopes: vec![
|
|
ScopeOption {
|
|
value: format!("{key}:read"),
|
|
label: "Read".to_string(),
|
|
requires_resource_path: *req,
|
|
},
|
|
ScopeOption {
|
|
value: format!("{key}:write"),
|
|
label: "Write".to_string(),
|
|
requires_resource_path: *req,
|
|
},
|
|
],
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
lazy_static! {
|
|
static ref ALL_SCOPES: Vec<ScopeDomain> = {
|
|
let mut groups = vec![ScopeDomain {
|
|
name: "Jobs".to_string(),
|
|
description: Some("Job management".to_string()),
|
|
scopes: vec![
|
|
ScopeOption {
|
|
value: "jobs:read".to_string(),
|
|
label: "Read".to_string(),
|
|
requires_resource_path: false,
|
|
},
|
|
ScopeOption {
|
|
value: "jobs:write".to_string(),
|
|
label: "Write".to_string(),
|
|
requires_resource_path: false,
|
|
},
|
|
ScopeOption {
|
|
value: "jobs:run:scripts".to_string(),
|
|
label: "Run scripts".to_string(),
|
|
requires_resource_path: true,
|
|
},
|
|
ScopeOption {
|
|
value: "jobs:run:flows".to_string(),
|
|
label: "Run flows".to_string(),
|
|
requires_resource_path: true,
|
|
},
|
|
],
|
|
}];
|
|
|
|
groups.extend(build_standard_scope_domains());
|
|
groups.extend(build_trigger_scope_domains());
|
|
|
|
groups
|
|
};
|
|
}
|
|
|
|
pub fn global_service() -> Router {
|
|
Router::new().route("/list/scopes", get(get_all_available_scopes))
|
|
}
|
|
|
|
async fn get_all_available_scopes() -> JsonResult<Vec<ScopeDomain>> {
|
|
Ok(Json(ALL_SCOPES.clone()))
|
|
}
|