diff --git a/backend/.sqlx/query-74f9031593b6c55819508fa32310e465c202fa3784fd179bcd8963217327a03b.json b/backend/.sqlx/query-74f9031593b6c55819508fa32310e465c202fa3784fd179bcd8963217327a03b.json new file mode 100644 index 0000000000..7440566057 --- /dev/null +++ b/backend/.sqlx/query-74f9031593b6c55819508fa32310e465c202fa3784fd179bcd8963217327a03b.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(SELECT 1 FROM native_trigger\n WHERE workspace_id = $1 AND service_name = $2 AND connection_path = $3)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + { + "Custom": { + "name": "native_trigger_service", + "kind": { + "Enum": [ + "nextcloud", + "google", + "github" + ] + } + } + }, + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "74f9031593b6c55819508fa32310e465c202fa3784fd179bcd8963217327a03b" +} diff --git a/backend/windmill-native-triggers/src/workspace_integrations.rs b/backend/windmill-native-triggers/src/workspace_integrations.rs index db621af978..240ac31355 100644 --- a/backend/windmill-native-triggers/src/workspace_integrations.rs +++ b/backend/windmill-native-triggers/src/workspace_integrations.rs @@ -222,6 +222,14 @@ async fn resolve_oauth_client( } } +/// Connecting writes the connection's variable and resource at `path`, so a path-scoped token must +/// cover both, whatever the user behind it may write. +#[cfg(feature = "native_trigger")] +fn check_connection_scopes(authed: &ApiAuthed, path: &str) -> Result<()> { + check_scopes(authed, || format!("variables:write:{path}"))?; + check_scopes(authed, || format!("resources:write:{path}")) +} + #[cfg(feature = "native_trigger")] fn default_connection_path(authed: &ApiAuthed, service_name: ServiceName) -> String { format!( @@ -247,6 +255,7 @@ async fn generate_connect_url( .resource_path .filter(|p| !p.is_empty()) .unwrap_or_else(|| default_connection_path(&authed, service_name)); + check_connection_scopes(&authed, &connection_path)?; // A GitHub OAuth app only redirects under its one registered callback, and the instance app's // is the resource connect callback, so its connections land on a page below that one. @@ -619,6 +628,7 @@ async fn oauth_callback( &authed.username, ) .await?; + check_connection_scopes(&authed, &resource_path)?; let (oauth_config, is_instance_shared) = resolve_oauth_client(&db, &workspace_id, service_name).await?; @@ -1070,7 +1080,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}"))?; + check_connection_scopes(&authed, &path)?; require_is_writer( &authed, &path, @@ -1121,6 +1131,25 @@ async fn delete_connection( ) .await; + // A trigger moved or created meanwhile was skipped above and still acts through this + // connection, so removing the connection would strand it. + let still_used = sqlx::query_scalar!( + "SELECT EXISTS(SELECT 1 FROM native_trigger + WHERE workspace_id = $1 AND service_name = $2 AND connection_path = $3)", + workspace_id, + service_name as ServiceName, + path, + ) + .fetch_one(&db) + .await? + .unwrap_or(false); + if still_used { + return Err(Error::BadRequest(format!( + "A trigger using {path} changed while disconnecting, so the connection was kept. \ + Disconnect again to remove it." + ))); + } + let mut tx = user_db.begin(&authed).await?; let account = cleanup_connection(&mut *tx, &workspace_id, &path).await?;