diff --git a/backend/.sqlx/query-387aebd876ef240f9c7c99c977d0b66587f50a70c24f331aa5838a754d13ada9.json b/backend/.sqlx/query-387aebd876ef240f9c7c99c977d0b66587f50a70c24f331aa5838a754d13ada9.json new file mode 100644 index 0000000000..d02146d4d7 --- /dev/null +++ b/backend/.sqlx/query-387aebd876ef240f9c7c99c977d0b66587f50a70c24f331aa5838a754d13ada9.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT script_path FROM native_trigger\n WHERE workspace_id = $1 AND service_name = $2 AND connection_path = $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "script_path", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + { + "Custom": { + "name": "native_trigger_service", + "kind": { + "Enum": [ + "nextcloud", + "google", + "github" + ] + } + } + }, + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "387aebd876ef240f9c7c99c977d0b66587f50a70c24f331aa5838a754d13ada9" +} diff --git a/backend/.sqlx/query-3419911d3acc46540ad818015390d4f366724e0d4040faeccf443ef9ce30665c.json b/backend/.sqlx/query-5294237a0bb6b23aed6915f430ac595c6f0d6f215725132fb7ef33bb1847831d.json similarity index 77% rename from backend/.sqlx/query-3419911d3acc46540ad818015390d4f366724e0d4040faeccf443ef9ce30665c.json rename to backend/.sqlx/query-5294237a0bb6b23aed6915f430ac595c6f0d6f215725132fb7ef33bb1847831d.json index ee4d590492..3c3870611e 100644 --- a/backend/.sqlx/query-3419911d3acc46540ad818015390d4f366724e0d4040faeccf443ef9ce30665c.json +++ b/backend/.sqlx/query-5294237a0bb6b23aed6915f430ac595c6f0d6f215725132fb7ef33bb1847831d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO workspace_integrations (workspace_id, service_name, oauth_data, created_by)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (workspace_id, service_name) DO UPDATE SET oauth_data = EXCLUDED.oauth_data\n WHERE workspace_integrations.oauth_data IS NULL", + "query": "INSERT INTO workspace_integrations (workspace_id, service_name, oauth_data, created_by)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (workspace_id, service_name) DO NOTHING", "describe": { "columns": [], "parameters": { @@ -24,5 +24,5 @@ }, "nullable": [] }, - "hash": "3419911d3acc46540ad818015390d4f366724e0d4040faeccf443ef9ce30665c" + "hash": "5294237a0bb6b23aed6915f430ac595c6f0d6f215725132fb7ef33bb1847831d" } diff --git a/backend/windmill-native-triggers/src/handler.rs b/backend/windmill-native-triggers/src/handler.rs index 5cea257955..49fae69bba 100644 --- a/backend/windmill-native-triggers/src/handler.rs +++ b/backend/windmill-native-triggers/src/handler.rs @@ -1,7 +1,7 @@ use crate::{ classify_read_failure, decrypt_oauth_data, delete_native_trigger, delete_token_by_hash, - get_native_trigger, list_native_triggers, lock::TriggerLock, map_external_error, - map_external_error_with, resolve_usable_connection, rotate_webhook_token, + get_native_trigger, list_native_triggers, list_usable_connections, lock::TriggerLock, + map_external_error, map_external_error_with, resolve_usable_connection, rotate_webhook_token, set_native_trigger_enabled, store_native_trigger, sync::EXTERNAL_TRIGGER_MISSING_ERROR, update_native_trigger_error, update_native_trigger_if_runnable_unchanged, webhook_token_label, webhook_token_scopes, External, ExternalReadFailure, NativeTrigger, NativeTriggerConfig, @@ -468,6 +468,28 @@ async fn get_native_trigger_handler( ) .await?; + // Reading the registration acts as the connection's account, like saving does. + let usable = match windmill_trigger.connection_path.as_deref() { + Some(path) => { + list_usable_connections(&mut tx, &workspace_id, service_name.integration_service()) + .await? + .iter() + .any(|c| c.path == path) + } + None => false, + }; + if !usable { + tx.commit().await?; + return Ok(Json(FullTriggerResponse { + windmill_data: windmill_trigger, + external_data: None, + external_error: Some(format!( + "You cannot use this trigger's {service_name} connection, so its registration was \ + not read." + )), + })); + } + let oauth_data: T::OAuthData = decrypt_oauth_data( &db, &workspace_id, diff --git a/backend/windmill-native-triggers/src/workspace_integrations.rs b/backend/windmill-native-triggers/src/workspace_integrations.rs index a8b32c8650..49647a3545 100644 --- a/backend/windmill-native-triggers/src/workspace_integrations.rs +++ b/backend/windmill-native-triggers/src/workspace_integrations.rs @@ -30,7 +30,7 @@ use windmill_common::{ }; #[cfg(feature = "native_trigger")] -use windmill_api_auth::{require_is_writer, ApiAuthed}; +use windmill_api_auth::{check_scopes, require_is_writer, ApiAuthed}; #[cfg(feature = "native_trigger")] use crate::{ @@ -709,8 +709,7 @@ async fn oauth_callback( sqlx::query!( "INSERT INTO workspace_integrations (workspace_id, service_name, oauth_data, created_by) VALUES ($1, $2, $3, $4) - ON CONFLICT (workspace_id, service_name) DO UPDATE SET oauth_data = EXCLUDED.oauth_data - WHERE workspace_integrations.oauth_data IS NULL", + ON CONFLICT (workspace_id, service_name) DO NOTHING", workspace_id, service_name as ServiceName, json!({ "instance_shared": true, "base_url": "" }), @@ -1005,9 +1004,10 @@ async fn get_instance_oauth_config( service_name: ServiceName, ) -> Result { if !is_instance_sharing_enabled(db, service_name).await? { - return Err(Error::BadRequest( - "Instance credential sharing is not enabled for this service".to_string(), - )); + return Err(Error::BadRequest(format!( + "No {service_name} OAuth app is configured for this workspace. A workspace admin can \ + configure one in the workspace settings, under native triggers." + ))); } let (client_id, client_secret) = @@ -1064,6 +1064,7 @@ async fn delete_connection( Path((workspace_id, service_name)): Path<(String, ServiceName)>, Query(ConnectionPathQuery { path }): Query, ) -> JsonResult { + check_scopes(&authed, || format!("variables:write:{path}"))?; require_is_writer( &authed, &path, @@ -1074,20 +1075,33 @@ async fn delete_connection( ) .await?; - let mut tx = user_db.begin(&authed).await?; - if !list_usable_connections(&mut tx, &workspace_id, service_name) - .await? - .iter() - .any(|c| c.path == path) - { + let mut tx = user_db.clone().begin(&authed).await?; + let usable = list_usable_connections(&mut tx, &workspace_id, service_name).await?; + tx.commit().await?; + if !usable.iter().any(|c| c.path == path) { return Err(Error::NotFound(format!( "No {service_name} connection at {path}" ))); } + // Disconnecting deletes these triggers, so a path-scoped token must cover each of them. + let trigger_paths = sqlx::query_scalar!( + "SELECT script_path FROM native_trigger + WHERE workspace_id = $1 AND service_name = $2 AND connection_path = $3", + workspace_id, + service_name as ServiceName, + path, + ) + .fetch_all(&db) + .await?; + for script_path in &trigger_paths { + check_scopes(&authed, || format!("native_triggers:write:{script_path}"))?; + } + // Before the connection goes: removing the registrations needs its token. delete_triggers_for_service(&db, &workspace_id, service_name, Some(&path)).await; + let mut tx = user_db.begin(&authed).await?; let account = cleanup_connection(&mut *tx, &workspace_id, &path).await?; audit_log(