From aea74445a31d30abb8030763db91e1829db772f0 Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:42:22 +0200 Subject: [PATCH] fix: add flow conversation token scope (#8903) * fix: add flow conversation token scope Co-Authored-By: Claude Opus 4.5 * refactor: make flow conversations scope plural Co-Authored-By: Claude Opus 4.5 * fix: update flow chat service import Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- backend/windmill-api-auth/src/scopes.rs | 50 ++++++++++++++++++- backend/windmill-api/openapi.yaml | 6 +-- backend/windmill-api/src/token.rs | 6 +++ .../conversations/FlowChatManager.svelte.ts | 10 ++-- 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/backend/windmill-api-auth/src/scopes.rs b/backend/windmill-api-auth/src/scopes.rs index 6b18b07c2b..b8f73fb95a 100644 --- a/backend/windmill-api-auth/src/scopes.rs +++ b/backend/windmill-api-auth/src/scopes.rs @@ -241,6 +241,7 @@ pub enum ScopeDomain { Jobs, Scripts, Flows, + FlowConversations, Apps, Variables, Resources, @@ -300,6 +301,7 @@ impl ScopeDomain { Self::Jobs => "jobs", Self::Scripts => "scripts", Self::Flows => "flows", + Self::FlowConversations => "flow_conversations", Self::Apps => "apps", Self::Variables => "variables", Self::Resources => "resources", @@ -348,6 +350,7 @@ impl ScopeDomain { "jobs" | "jobs_u" => Some(Self::Jobs), "scripts" => Some(Self::Scripts), "flows" => Some(Self::Flows), + "flow_conversations" => Some(Self::FlowConversations), "apps" | "apps_u" => Some(Self::Apps), "variables" => Some(Self::Variables), "resources" => Some(Self::Resources), @@ -764,6 +767,12 @@ mod tests { assert_eq!(domain, ScopeDomain::Scripts); assert_eq!(kind, None); assert_eq!(route_suffix, Some("scripts/test_script".to_string())); + + let (domain, kind, route_suffix) = + extract_domain_from_route("/api/w/test_workspace/flow_conversations/list").unwrap(); + assert_eq!(domain, ScopeDomain::FlowConversations); + assert_eq!(kind, None); + assert_eq!(route_suffix, Some("flow_conversations/list".to_string())); } #[test] @@ -788,11 +797,50 @@ mod tests { ScopeDomain::from_str("agent_workers"), Some(ScopeDomain::AgentWorkers) ); + assert_eq!( + ScopeDomain::from_str("flow_conversations"), + Some(ScopeDomain::FlowConversations) + ); - // Test that string conversion works both ways + // Test canonical string conversion assert_eq!(ScopeDomain::Acls.as_str(), "acls"); assert_eq!(ScopeDomain::RawApps.as_str(), "raw_apps"); assert_eq!(ScopeDomain::AgentWorkers.as_str(), "agent_workers"); + assert_eq!( + ScopeDomain::FlowConversations.as_str(), + "flow_conversations" + ); + } + + #[test] + fn test_flow_conversations_scope_access() { + let read_scopes = vec!["flow_conversations:read".to_string()]; + assert!(check_route_access( + &read_scopes, + "/api/w/test_workspace/flow_conversations/list", + "GET" + ) + .is_ok()); + assert!(check_route_access( + &read_scopes, + "/api/w/test_workspace/flow_conversations/123/messages", + "GET" + ) + .is_ok()); + assert!(check_route_access( + &read_scopes, + "/api/w/test_workspace/flow_conversations/delete/123", + "DELETE" + ) + .is_err()); + + let write_scopes = vec!["flow_conversations:write".to_string()]; + assert!(check_route_access( + &write_scopes, + "/api/w/test_workspace/flow_conversations/delete/123", + "DELETE" + ) + .is_ok()); } #[test] diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 86c8bbf6ab..5f688d23b2 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -9391,7 +9391,7 @@ paths: summary: list flow conversations operationId: listFlowConversations tags: - - flow_conversation + - flow_conversations parameters: - $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/Page" @@ -9416,7 +9416,7 @@ paths: summary: delete flow conversation operationId: deleteFlowConversation tags: - - flow_conversation + - flow_conversations parameters: - $ref: "#/components/parameters/WorkspaceId" - name: conversation_id @@ -9439,7 +9439,7 @@ paths: summary: list conversation messages operationId: listConversationMessages tags: - - flow_conversation + - flow_conversations parameters: - $ref: "#/components/parameters/WorkspaceId" - $ref: "#/components/parameters/Page" diff --git a/backend/windmill-api/src/token.rs b/backend/windmill-api/src/token.rs index 25978eb873..61baf5e3c4 100644 --- a/backend/windmill-api/src/token.rs +++ b/backend/windmill-api/src/token.rs @@ -65,6 +65,12 @@ fn build_standard_scope_domains() -> Vec { "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), diff --git a/frontend/src/lib/components/flows/conversations/FlowChatManager.svelte.ts b/frontend/src/lib/components/flows/conversations/FlowChatManager.svelte.ts index d91b27d4c0..66e5b09bc7 100644 --- a/frontend/src/lib/components/flows/conversations/FlowChatManager.svelte.ts +++ b/frontend/src/lib/components/flows/conversations/FlowChatManager.svelte.ts @@ -1,5 +1,5 @@ import type { FlowConversation, FlowConversationMessage } from '$lib/gen/types.gen' -import { FlowConversationService, JobService } from '$lib/gen' +import { FlowConversationsService, JobService } from '$lib/gen' import { sendUserToast } from '$lib/toast' import { waitJob } from '$lib/components/waitJob' import { tick } from 'svelte' @@ -167,7 +167,7 @@ export class FlowChatManager { private async deleteConversation(conversationId: string) { try { this.deletingConversationId = conversationId - await FlowConversationService.deleteFlowConversation({ + await FlowConversationsService.deleteFlowConversation({ workspace: get(workspaceStore)!, conversationId }) @@ -217,7 +217,7 @@ export class FlowChatManager { if (!get(workspaceStore) || !this.#path) return [] try { - const response = await FlowConversationService.listFlowConversations({ + const response = await FlowConversationsService.listFlowConversations({ workspace: get(workspaceStore)!, flowPath: this.#path, page: page, @@ -252,7 +252,7 @@ export class FlowChatManager { try { const previousScrollHeight = this.messagesContainer?.scrollHeight || 0 - const response = await FlowConversationService.listConversationMessages({ + const response = await FlowConversationsService.listConversationMessages({ workspace: get(workspaceStore)!, conversationId: conversationIdToUse, page: pageToFetch, @@ -339,7 +339,7 @@ export class FlowChatManager { try { const lastId = this.messages[this.messages.length - 1].id - const response = await FlowConversationService.listConversationMessages({ + const response = await FlowConversationsService.listConversationMessages({ workspace: get(workspaceStore)!, conversationId: conversationId, page: 1,