diff --git a/Cargo.lock b/Cargo.lock index c61d3d54..dc028071 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11352,7 +11352,6 @@ dependencies = [ name = "yaak-commands" version = "0.0.0" dependencies = [ - "log 0.4.29", "serde_json", "tempfile", "thiserror 2.0.17", diff --git a/crates-tauri/yaak-app-client/src/grpc.rs b/crates-tauri/yaak-app-client/src/grpc.rs index 4981c9e2..0f95403a 100644 --- a/crates-tauri/yaak-app-client/src/grpc.rs +++ b/crates-tauri/yaak-app-client/src/grpc.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use crate::PluginContextExt; use crate::error::Result; -use crate::models_ext::QueryManagerExt; use KeyAndValueRef::{Ascii, Binary}; use tauri::{Manager, Runtime, WebviewWindow}; use yaak_grpc::{KeyAndValueRef, MetadataMap}; @@ -21,22 +20,6 @@ pub(crate) fn metadata_to_map(metadata: MetadataMap) -> BTreeMap entries } -pub(crate) fn resolve_grpc_request( - window: &WebviewWindow, - request: &GrpcRequest, -) -> Result<(GrpcRequest, String)> { - let mut new_request = request.clone(); - - let (authentication_type, authentication, authentication_context_id) = - window.db().resolve_auth_for_grpc_request(request)?; - new_request.authentication_type = authentication_type; - new_request.authentication = authentication; - - let metadata = window.db().resolve_metadata_for_grpc_request(request)?; - new_request.metadata = metadata; - - Ok((new_request, authentication_context_id)) -} pub(crate) async fn build_metadata( window: &WebviewWindow, diff --git a/crates-tauri/yaak-app-client/src/http_request.rs b/crates-tauri/yaak-app-client/src/http_request.rs index 4bdbff5d..efa6cfbc 100644 --- a/crates-tauri/yaak-app-client/src/http_request.rs +++ b/crates-tauri/yaak-app-client/src/http_request.rs @@ -179,19 +179,3 @@ async fn send_http_request_inner( Ok(SentHttpRequest { response: result.response, body: result.response_body }) } -pub fn resolve_http_request( - window: &WebviewWindow, - request: &HttpRequest, -) -> Result<(HttpRequest, String)> { - let mut new_request = request.clone(); - - let (authentication_type, authentication, authentication_context_id) = - window.db().resolve_auth_for_http_request(request)?; - new_request.authentication_type = authentication_type; - new_request.authentication = authentication; - - let headers = window.db().resolve_headers_for_http_request(request)?; - new_request.headers = headers; - - Ok((new_request, authentication_context_id)) -} diff --git a/crates-tauri/yaak-app-client/src/lib.rs b/crates-tauri/yaak-app-client/src/lib.rs index 00e976c6..76c75fbc 100644 --- a/crates-tauri/yaak-app-client/src/lib.rs +++ b/crates-tauri/yaak-app-client/src/lib.rs @@ -2,18 +2,17 @@ extern crate core; use crate::encoding::read_response_body; use crate::error::Error::GenericError; use crate::error::Result; -use crate::grpc::{build_metadata, metadata_to_map, resolve_grpc_request}; -use crate::http_request::{resolve_http_request, send_http_request}; +use crate::grpc::{build_metadata, metadata_to_map}; +use crate::http_request::send_http_request; use crate::import::{import_data, import_url}; use crate::models_ext::{BlobManagerExt, QueryManagerExt}; use crate::notifications::YaakNotifier; -use crate::render::{render_grpc_request, render_json_value, render_template}; +use crate::render::{render_grpc_request, render_template}; use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater}; use crate::uri_scheme::handle_deep_link; use error::Result as YaakResult; use eventsource_client::{EventParser, SSE}; use log::{debug, error, info, warn}; -use std::collections::HashMap; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; @@ -31,25 +30,20 @@ use tokio::task::block_in_place; use tokio::time; use yaak::send::ResponseBody; use yaak_commands::responses::locate_response_body; +use yaak_commands::resolve::resolve_grpc_request; use yaak_common::command::new_checked_command; use yaak_crypto::manager::EncryptionManager; use yaak_grpc::manager::{GrpcConfig, GrpcHandle}; use yaak_grpc::{Code, ServiceDefinition}; use yaak_mac_window::AppHandleMacWindowExt; use yaak_models::models::{ - AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent, + CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType, HttpRequest, HttpResponse, HttpResponseState, Workspace, }; use yaak_models::util::{BatchUpsertResult, UpdateSource}; use yaak_plugins::events::{ - CallFolderActionArgs, CallFolderActionRequest, CallGrpcRequestActionArgs, - CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest, - CallWebsocketRequestActionArgs, CallWebsocketRequestActionRequest, CallWorkspaceActionArgs, - CallWorkspaceActionRequest, Color, ErrorResponse, FilterResponse, GetFolderActionsResponse, - GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, - GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, - GetWebsocketRequestActionsResponse, GetWorkspaceActionsResponse, InternalEvent, - InternalEventPayload, JsonPrimitive, PluginContext, RenderPurpose, ShowToastRequest, + Color, ErrorResponse, FilterResponse, InternalEvent, InternalEventPayload, PluginContext, + RenderPurpose, ShowToastRequest, }; use yaak_plugins::manager::PluginManager; use yaak_plugins::template_callback::PluginTemplateCallback; @@ -244,7 +238,8 @@ async fn cmd_grpc_reflect( grpc_handle: State<'_, Mutex>, ) -> YaakResult> { let unrendered_request = app_handle.db().get_grpc_request(request_id)?; - let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?; + let (resolved_request, auth_context_id) = + resolve_grpc_request(&window.db(), &unrendered_request)?; let environment_chain = app_handle.db().resolve_environments( &unrendered_request.workspace_id, @@ -304,7 +299,8 @@ async fn cmd_grpc_go( grpc_handle: State<'_, Mutex>, ) -> YaakResult { let unrendered_request = app_handle.db().get_grpc_request(request_id)?; - let (resolved_request, auth_context_id) = resolve_grpc_request(&window, &unrendered_request)?; + let (resolved_request, auth_context_id) = + resolve_grpc_request(&window.db(), &unrendered_request)?; let environment_chain = app_handle.db().resolve_environments( &unrendered_request.workspace_id, unrendered_request.folder_id.as_deref(), @@ -1028,262 +1024,19 @@ async fn cmd_import_url( import_url(&window, url).await } -async fn cmd_http_request_actions( - window: WebviewWindow, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult> { - Ok(plugin_manager.get_http_request_actions(&window.plugin_context()).await?) -} -async fn cmd_websocket_request_actions( - window: WebviewWindow, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult> { - Ok(plugin_manager.get_websocket_request_actions(&window.plugin_context()).await?) -} -async fn cmd_call_websocket_request_action( - window: WebviewWindow, - req: CallWebsocketRequestActionRequest, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult<()> { - let websocket_request = window.db().get_websocket_request(&req.args.websocket_request.id)?; - Ok(plugin_manager - .call_websocket_request_action( - &window.plugin_context(), - CallWebsocketRequestActionRequest { - args: CallWebsocketRequestActionArgs { websocket_request }, - ..req - }, - ) - .await?) -} -async fn cmd_workspace_actions( - window: WebviewWindow, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult> { - Ok(plugin_manager.get_workspace_actions(&window.plugin_context()).await?) -} -async fn cmd_call_workspace_action( - window: WebviewWindow, - req: CallWorkspaceActionRequest, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult<()> { - let workspace = window.db().get_workspace(&req.args.workspace.id)?; - Ok(plugin_manager - .call_workspace_action( - &window.plugin_context(), - CallWorkspaceActionRequest { args: CallWorkspaceActionArgs { workspace }, ..req }, - ) - .await?) -} -async fn cmd_folder_actions( - window: WebviewWindow, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult> { - Ok(plugin_manager.get_folder_actions(&window.plugin_context()).await?) -} -async fn cmd_call_folder_action( - window: WebviewWindow, - req: CallFolderActionRequest, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult<()> { - let folder = window.db().get_folder(&req.args.folder.id)?; - Ok(plugin_manager - .call_folder_action( - &window.plugin_context(), - CallFolderActionRequest { args: CallFolderActionArgs { folder }, ..req }, - ) - .await?) -} -async fn cmd_grpc_request_actions( - window: WebviewWindow, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult> { - Ok(plugin_manager.get_grpc_request_actions(&window.plugin_context()).await?) -} -async fn cmd_get_http_authentication_summaries( - window: WebviewWindow, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult> { - let results = - plugin_manager.get_http_authentication_summaries(&window.plugin_context()).await?; - Ok(results.into_iter().map(|(_, a)| a).collect()) -} -async fn cmd_get_http_authentication_config( - window: WebviewWindow, - app_handle: AppHandle, - plugin_manager: State<'_, PluginManager>, - encryption_manager: State<'_, EncryptionManager>, - auth_name: &str, - values: HashMap, - model: AnyModel, - environment_id: Option<&str>, -) -> YaakResult { - // Extract workspace_id and folder_id from the model to resolve the environment chain - let (workspace_id, folder_id) = match &model { - AnyModel::HttpRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()), - AnyModel::GrpcRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()), - AnyModel::WebsocketRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()), - AnyModel::Folder(f) => (f.workspace_id.clone(), f.folder_id.clone()), - AnyModel::Workspace(w) => (w.id.clone(), None), - _ => return Err(GenericError("Unsupported model type for authentication config".into())), - }; - // Resolve environment chain and render the values for token lookup - let environment_chain = app_handle.db().resolve_environments( - &workspace_id, - folder_id.as_deref(), - environment_id, - )?; - let plugin_manager_arc = Arc::new((*plugin_manager).clone()); - let encryption_manager_arc = Arc::new((*encryption_manager).clone()); - let cb = PluginTemplateCallback::new( - plugin_manager_arc, - encryption_manager_arc, - &window.plugin_context(), - RenderPurpose::Preview, - ); - // Convert HashMap to serde_json::Value for rendering - let values_json: serde_json::Value = serde_json::to_value(&values)?; - let rendered_json = - render_json_value(values_json, environment_chain, &cb, &RenderOptions::return_empty()) - .await?; - // Convert back to HashMap - let rendered_values: HashMap = serde_json::from_value(rendered_json)?; - Ok(plugin_manager - .get_http_authentication_config( - &window.plugin_context(), - auth_name, - rendered_values, - model.id(), - ) - .await?) -} - -async fn cmd_call_http_request_action( - window: WebviewWindow, - req: CallHttpRequestActionRequest, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult<()> { - Ok(plugin_manager - .call_http_request_action( - &window.plugin_context(), - CallHttpRequestActionRequest { - args: CallHttpRequestActionArgs { - http_request: resolve_http_request(&window, &req.args.http_request)?.0, - ..req.args - }, - ..req - }, - ) - .await?) -} - -async fn cmd_call_grpc_request_action( - window: WebviewWindow, - req: CallGrpcRequestActionRequest, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult<()> { - Ok(plugin_manager - .call_grpc_request_action( - &window.plugin_context(), - CallGrpcRequestActionRequest { - args: CallGrpcRequestActionArgs { - grpc_request: resolve_grpc_request(&window, &req.args.grpc_request)?.0, - ..req.args - }, - ..req - }, - ) - .await?) -} - -async fn cmd_call_http_authentication_action( - window: WebviewWindow, - app_handle: AppHandle, - plugin_manager: State<'_, PluginManager>, - encryption_manager: State<'_, EncryptionManager>, - auth_name: &str, - action_index: i32, - values: HashMap, - model: AnyModel, - environment_id: Option<&str>, -) -> YaakResult<()> { - // Extract workspace_id and folder_id from the model to resolve the environment chain - let (workspace_id, folder_id) = match &model { - AnyModel::HttpRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()), - AnyModel::GrpcRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()), - AnyModel::WebsocketRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()), - AnyModel::Folder(f) => (f.workspace_id.clone(), f.folder_id.clone()), - AnyModel::Workspace(w) => (w.id.clone(), None), - _ => return Err(GenericError("Unsupported model type for authentication action".into())), - }; - - // Resolve environment chain and render the values - let environment_chain = app_handle.db().resolve_environments( - &workspace_id, - folder_id.as_deref(), - environment_id, - )?; - let plugin_manager_arc = Arc::new((*plugin_manager).clone()); - let encryption_manager_arc = Arc::new((*encryption_manager).clone()); - let cb = PluginTemplateCallback::new( - plugin_manager_arc, - encryption_manager_arc, - &window.plugin_context(), - RenderPurpose::Send, - ); - - // Convert HashMap to serde_json::Value for rendering - let values_json: serde_json::Value = serde_json::to_value(&values)?; - let rendered_json = - render_json_value(values_json, environment_chain, &cb, &RenderOptions::throw()).await?; - - // Convert back to HashMap - let rendered_values: HashMap = serde_json::from_value(rendered_json)?; - - Ok(plugin_manager - .call_http_authentication_action( - &window.plugin_context(), - auth_name, - action_index, - rendered_values, - &model.id(), - ) - .await?) -} - -async fn cmd_curl_to_request( - window: WebviewWindow, - command: &str, - plugin_manager: State<'_, PluginManager>, - workspace_id: &str, -) -> YaakResult { - let import_result = plugin_manager.import_data(&window.plugin_context(), command).await?; - - Ok(import_result - .resources - .http_requests - .get(0) - .ok_or(GenericError("No curl command found".to_string())) - .map(|r| { - let mut request = r.clone(); - request.workspace_id = workspace_id.into(); - request.id = "".to_string(); - request - })?) -} /// Decodes base64 and writes the bytes to a file the user picked. /// @@ -1379,17 +1132,6 @@ async fn cmd_send_http_request( Ok(r) } -async fn cmd_reload_plugins( - app_handle: AppHandle, - window: WebviewWindow, - plugin_manager: State<'_, PluginManager>, -) -> YaakResult> { - let plugins = app_handle.db().list_plugins()?; - let plugin_context = - PluginContext::new(Some(window.label().to_string()), window.workspace_id()); - let errors = plugin_manager.initialize_all_plugins(plugins, &plugin_context).await; - Ok(errors) -} async fn cmd_new_child_window( parent_window: WebviewWindow, diff --git a/crates-tauri/yaak-app-client/src/rpc_ext.rs b/crates-tauri/yaak-app-client/src/rpc_ext.rs index 9b70d105..07313b49 100644 --- a/crates-tauri/yaak-app-client/src/rpc_ext.rs +++ b/crates-tauri/yaak-app-client/src/rpc_ext.rs @@ -42,7 +42,9 @@ use yaak_models::models::{ use yaak_models::query_manager::QueryManager; use yaak_models::util::BatchUpsertResult; use yaak_plugins::events::{ - FilterResponse, JsonPrimitive, RenderPurpose, GetFolderActionsResponse, GetGrpcRequestActionsResponse, + CallFolderActionRequest, CallGrpcRequestActionRequest, CallHttpRequestActionRequest, + CallWebsocketRequestActionRequest, CallWorkspaceActionRequest, FilterResponse, ImportResponse, + JsonPrimitive, RenderPurpose, GetFolderActionsResponse, GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, GetThemesResponse, GetWebsocketRequestActionsResponse, @@ -106,28 +108,35 @@ impl Host for ClientCtx { } } +impl ClientCtx { + /// The plugin runtime this window talks to. Only the `PluginHost` impl + /// below uses it; everything else goes through the trait. + fn pm(&self) -> State<'_, PluginManager> { + self.window.state::() + } +} + /// The desktop answers all of these out of the `PluginManager` it already /// runs — the Node sidecar. Each is a delegation, which is the point: the /// operations are what the handlers need, and this is one host's way of /// providing them. impl PluginHost for ClientCtx { async fn loaded_plugin_metadata(&self, directory: &str) -> Option { - let manager = self.window.state::(); - let handle = manager.get_plugin_by_dir(directory).await?; + let handle = self.pm().get_plugin_by_dir(directory).await?; Some(handle.info()) } async fn take_plugin_init_errors(&self) -> Vec<(String, String)> { - self.window.state::().take_init_errors().await + self.pm().take_init_errors().await } async fn resolve_plugins(&self, plugins: Vec) -> Vec { - self.window.state::().resolve_plugins_for_runtime_from_db(plugins).await + self.pm().resolve_plugins_for_runtime_from_db(plugins).await } fn template_callback(&self, purpose: RenderPurpose) -> impl TemplateCallback { PluginTemplateCallback::new( - Arc::new((*self.window.state::()).clone()), + Arc::new((*self.pm()).clone()), Arc::new(self.encryption_manager().clone()), &self.plugin_context(), purpose, @@ -158,11 +167,118 @@ impl PluginHost for ClientCtx { } async fn themes(&self) -> yaak_commands::Result> { - Ok(self.window.state::().get_themes(&self.plugin_context()).await?) + Ok(self.pm().get_themes(&self.plugin_context()).await?) + } + + async fn http_request_actions( + &self, + ) -> yaak_commands::Result> { + Ok(self.pm().get_http_request_actions(&self.plugin_context()).await?) + } + + async fn websocket_request_actions( + &self, + ) -> yaak_commands::Result> { + Ok(self.pm().get_websocket_request_actions(&self.plugin_context()).await?) + } + + async fn grpc_request_actions( + &self, + ) -> yaak_commands::Result> { + Ok(self.pm().get_grpc_request_actions(&self.plugin_context()).await?) + } + + async fn workspace_actions(&self) -> yaak_commands::Result> { + Ok(self.pm().get_workspace_actions(&self.plugin_context()).await?) + } + + async fn folder_actions(&self) -> yaak_commands::Result> { + Ok(self.pm().get_folder_actions(&self.plugin_context()).await?) + } + + async fn call_http_request_action( + &self, + req: CallHttpRequestActionRequest, + ) -> yaak_commands::Result<()> { + Ok(self.pm().call_http_request_action(&self.plugin_context(), req).await?) + } + + async fn call_grpc_request_action( + &self, + req: CallGrpcRequestActionRequest, + ) -> yaak_commands::Result<()> { + Ok(self.pm().call_grpc_request_action(&self.plugin_context(), req).await?) + } + + async fn call_websocket_request_action( + &self, + req: CallWebsocketRequestActionRequest, + ) -> yaak_commands::Result<()> { + Ok(self.pm().call_websocket_request_action(&self.plugin_context(), req).await?) + } + + async fn call_workspace_action( + &self, + req: CallWorkspaceActionRequest, + ) -> yaak_commands::Result<()> { + Ok(self.pm().call_workspace_action(&self.plugin_context(), req).await?) + } + + async fn call_folder_action( + &self, + req: CallFolderActionRequest, + ) -> yaak_commands::Result<()> { + Ok(self.pm().call_folder_action(&self.plugin_context(), req).await?) + } + + async fn http_authentication_summaries( + &self, + ) -> yaak_commands::Result> { + let results = self.pm().get_http_authentication_summaries(&self.plugin_context()).await?; + Ok(results.into_iter().map(|(_, a)| a).collect()) + } + + async fn http_authentication_config( + &self, + auth_name: &str, + values: HashMap, + model_id: &str, + ) -> yaak_commands::Result { + Ok(self + .pm() + .get_http_authentication_config(&self.plugin_context(), auth_name, values, model_id) + .await?) + } + + async fn call_http_authentication_action( + &self, + auth_name: &str, + action_index: i32, + values: HashMap, + model_id: &str, + ) -> yaak_commands::Result<()> { + Ok(self + .pm() + .call_http_authentication_action( + &self.plugin_context(), + auth_name, + action_index, + values, + model_id, + ) + .await?) + } + + async fn import_data(&self, content: &str) -> yaak_commands::Result { + Ok(self.pm().import_data(&self.plugin_context(), content).await?) + } + + async fn reload_plugins(&self, plugins: Vec) -> Vec<(String, String)> { + self.pm().initialize_all_plugins(plugins, &self.plugin_context()).await } async fn encrypt_secure_template(&self, template: &str) -> yaak_commands::Result { - let plugin_manager = Arc::new((*self.window.state::()).clone()); + let plugin_manager = Arc::new((*self.pm()).clone()); let encryption_manager = Arc::new(self.encryption_manager().clone()); Ok(encrypt_secure_template_function( plugin_manager, @@ -333,36 +449,36 @@ async fn cmd_import_url(ctx: ClientCtx, req: CmdImportUrlReq) -> Ok(crate::cmd_import_url(ctx.window.clone(), &req.url).await?) } -async fn cmd_http_request_actions(ctx: ClientCtx, _req: CmdHttpRequestActionsReq) -> Result> { - Ok(crate::cmd_http_request_actions(ctx.window.clone(), ctx.window.app_handle().state::()).await?) +async fn cmd_http_request_actions(ctx: ClientCtx, req: CmdHttpRequestActionsReq) -> Result> { + Ok(yaak_commands::actions::cmd_http_request_actions(ctx, req).await?) } -async fn cmd_websocket_request_actions(ctx: ClientCtx, _req: CmdWebsocketRequestActionsReq) -> Result> { - Ok(crate::cmd_websocket_request_actions(ctx.window.clone(), ctx.window.app_handle().state::()).await?) +async fn cmd_websocket_request_actions(ctx: ClientCtx, req: CmdWebsocketRequestActionsReq) -> Result> { + Ok(yaak_commands::actions::cmd_websocket_request_actions(ctx, req).await?) } async fn cmd_call_websocket_request_action(ctx: ClientCtx, req: CmdCallWebsocketRequestActionReq) -> Result<()> { - Ok(crate::cmd_call_websocket_request_action(ctx.window.clone(), req.req, ctx.window.app_handle().state::()).await?) + Ok(yaak_commands::actions::cmd_call_websocket_request_action(ctx, req).await?) } -async fn cmd_workspace_actions(ctx: ClientCtx, _req: CmdWorkspaceActionsReq) -> Result> { - Ok(crate::cmd_workspace_actions(ctx.window.clone(), ctx.window.app_handle().state::()).await?) +async fn cmd_workspace_actions(ctx: ClientCtx, req: CmdWorkspaceActionsReq) -> Result> { + Ok(yaak_commands::actions::cmd_workspace_actions(ctx, req).await?) } async fn cmd_call_workspace_action(ctx: ClientCtx, req: CmdCallWorkspaceActionReq) -> Result<()> { - Ok(crate::cmd_call_workspace_action(ctx.window.clone(), req.req, ctx.window.app_handle().state::()).await?) + Ok(yaak_commands::actions::cmd_call_workspace_action(ctx, req).await?) } -async fn cmd_folder_actions(ctx: ClientCtx, _req: CmdFolderActionsReq) -> Result> { - Ok(crate::cmd_folder_actions(ctx.window.clone(), ctx.window.app_handle().state::()).await?) +async fn cmd_folder_actions(ctx: ClientCtx, req: CmdFolderActionsReq) -> Result> { + Ok(yaak_commands::actions::cmd_folder_actions(ctx, req).await?) } async fn cmd_call_folder_action(ctx: ClientCtx, req: CmdCallFolderActionReq) -> Result<()> { - Ok(crate::cmd_call_folder_action(ctx.window.clone(), req.req, ctx.window.app_handle().state::()).await?) + Ok(yaak_commands::actions::cmd_call_folder_action(ctx, req).await?) } -async fn cmd_grpc_request_actions(ctx: ClientCtx, _req: CmdGrpcRequestActionsReq) -> Result> { - Ok(crate::cmd_grpc_request_actions(ctx.window.clone(), ctx.window.app_handle().state::()).await?) +async fn cmd_grpc_request_actions(ctx: ClientCtx, req: CmdGrpcRequestActionsReq) -> Result> { + Ok(yaak_commands::actions::cmd_grpc_request_actions(ctx, req).await?) } async fn cmd_template_function_summaries(ctx: ClientCtx, req: CmdTemplateFunctionSummariesReq) -> Result> { @@ -373,28 +489,28 @@ async fn cmd_template_function_config(ctx: ClientCtx, req: CmdTem Ok(yaak_commands::templates::cmd_template_function_config(ctx, req).await?) } -async fn cmd_get_http_authentication_summaries(ctx: ClientCtx, _req: CmdGetHttpAuthenticationSummariesReq) -> Result> { - Ok(crate::cmd_get_http_authentication_summaries(ctx.window.clone(), ctx.window.app_handle().state::()).await?) +async fn cmd_get_http_authentication_summaries(ctx: ClientCtx, req: CmdGetHttpAuthenticationSummariesReq) -> Result> { + Ok(yaak_commands::auth::cmd_get_http_authentication_summaries(ctx, req).await?) } async fn cmd_get_http_authentication_config(ctx: ClientCtx, req: CmdGetHttpAuthenticationConfigReq) -> Result { - Ok(crate::cmd_get_http_authentication_config(ctx.window.clone(), ctx.window.app_handle().clone(), ctx.window.app_handle().state::(), ctx.window.app_handle().state::(), &req.auth_name, req.values, req.model, req.environment_id.as_deref()).await?) + Ok(yaak_commands::auth::cmd_get_http_authentication_config(ctx, req).await?) } async fn cmd_call_http_request_action(ctx: ClientCtx, req: CmdCallHttpRequestActionReq) -> Result<()> { - Ok(crate::cmd_call_http_request_action(ctx.window.clone(), req.req, ctx.window.app_handle().state::()).await?) + Ok(yaak_commands::actions::cmd_call_http_request_action(ctx, req).await?) } async fn cmd_call_grpc_request_action(ctx: ClientCtx, req: CmdCallGrpcRequestActionReq) -> Result<()> { - Ok(crate::cmd_call_grpc_request_action(ctx.window.clone(), req.req, ctx.window.app_handle().state::()).await?) + Ok(yaak_commands::actions::cmd_call_grpc_request_action(ctx, req).await?) } async fn cmd_call_http_authentication_action(ctx: ClientCtx, req: CmdCallHttpAuthenticationActionReq) -> Result<()> { - Ok(crate::cmd_call_http_authentication_action(ctx.window.clone(), ctx.window.app_handle().clone(), ctx.window.app_handle().state::(), ctx.window.app_handle().state::(), &req.auth_name, req.action_index, req.values, req.model, req.environment_id.as_deref()).await?) + Ok(yaak_commands::auth::cmd_call_http_authentication_action(ctx, req).await?) } async fn cmd_curl_to_request(ctx: ClientCtx, req: CmdCurlToRequestReq) -> Result { - Ok(crate::cmd_curl_to_request(ctx.window.clone(), &req.command, ctx.window.app_handle().state::(), &req.workspace_id).await?) + Ok(yaak_commands::actions::cmd_curl_to_request(ctx, req).await?) } async fn cmd_export_data(ctx: ClientCtx, req: CmdExportDataReq) -> Result<()> { @@ -413,8 +529,8 @@ async fn cmd_send_http_request(ctx: ClientCtx, req: CmdSendHttpRe Ok(crate::cmd_send_http_request(ctx.window.app_handle().clone(), ctx.window.clone(), req.environment_id.as_deref(), req.cookie_jar_id.as_deref(), req.request_id).await?) } -async fn cmd_reload_plugins(ctx: ClientCtx, _req: CmdReloadPluginsReq) -> Result> { - Ok(crate::cmd_reload_plugins(ctx.window.app_handle().clone(), ctx.window.clone(), ctx.window.app_handle().state::()).await?) +async fn cmd_reload_plugins(ctx: ClientCtx, req: CmdReloadPluginsReq) -> Result> { + Ok(yaak_commands::actions::cmd_reload_plugins(ctx, req).await?) } async fn cmd_plugin_info(ctx: ClientCtx, req: CmdPluginInfoReq) -> Result { diff --git a/crates-tauri/yaak-app-client/src/ws_ext.rs b/crates-tauri/yaak-app-client/src/ws_ext.rs index 2f79e770..ef8a4e36 100644 --- a/crates-tauri/yaak-app-client/src/ws_ext.rs +++ b/crates-tauri/yaak-app-client/src/ws_ext.rs @@ -18,7 +18,7 @@ use yaak_http::cookies::CookieStore; use yaak_http::path_placeholders::apply_path_placeholders; use yaak_models::models::{ HttpResponseHeader, WebsocketConnection, WebsocketConnectionState, WebsocketEvent, - WebsocketEventType, WebsocketRequest, + WebsocketEventType, }; use yaak_models::util::UpdateSource; use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose}; @@ -27,6 +27,7 @@ use yaak_plugins::template_callback::PluginTemplateCallback; use yaak_templates::strip_json_comments::maybe_strip_json_comments; use yaak_templates::{RenderErrorBehavior, RenderOptions}; use yaak_tls::find_client_certificate; +use yaak_commands::resolve::resolve_websocket_request; use yaak_ws::{WebsocketManager, render_websocket_request}; pub async fn cmd_ws_send( @@ -75,7 +76,7 @@ async fn send_websocket_message( environment_id, )?; let (resolved_request, _auth_context_id) = - resolve_websocket_request(&window, &unrendered_request)?; + resolve_websocket_request(&window.db(), &unrendered_request)?; let plugin_manager = Arc::new((*app_handle.state::()).clone()); let encryption_manager = Arc::new((*app_handle.state::()).clone()); let request = render_websocket_request( @@ -154,7 +155,7 @@ pub async fn cmd_ws_connect( app_handle.db().resolve_settings_for_websocket_request(&unrendered_request)?; let settings = app_handle.db().get_settings(); let (resolved_request, auth_context_id) = - resolve_websocket_request(&window, &unrendered_request)?; + resolve_websocket_request(&window.db(), &unrendered_request)?; let plugin_manager = Arc::new((*app_handle.state::()).clone()); let encryption_manager = Arc::new((*app_handle.state::()).clone()); let request = render_websocket_request( @@ -454,23 +455,6 @@ pub async fn cmd_ws_connect( Ok(connection) } -/// Resolve inherited authentication and headers for a websocket request -fn resolve_websocket_request( - window: &WebviewWindow, - request: &WebsocketRequest, -) -> Result<(WebsocketRequest, String)> { - let mut new_request = request.clone(); - - let (authentication_type, authentication, authentication_context_id) = - window.db().resolve_auth_for_websocket_request(request)?; - new_request.authentication_type = authentication_type; - new_request.authentication = authentication; - - let headers = window.db().resolve_headers_for_websocket_request(request)?; - new_request.headers = headers; - - Ok((new_request, authentication_context_id)) -} /// Convert WS URL to HTTP URL for cookie filtering /// WebSocket upgrade requests are HTTP requests initially, so HttpOnly cookies should apply diff --git a/crates/yaak-commands/Cargo.toml b/crates/yaak-commands/Cargo.toml index ce120d9c..59d011a6 100644 --- a/crates/yaak-commands/Cargo.toml +++ b/crates/yaak-commands/Cargo.toml @@ -6,10 +6,8 @@ authors = ["Gregory Schier"] publish = false [dependencies] -log = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true, features = ["rt"] } yaak = { workspace = true } yaak-core = { workspace = true } yaak-crypto = { workspace = true } diff --git a/crates/yaak-commands/src/actions.rs b/crates/yaak-commands/src/actions.rs new file mode 100644 index 00000000..97b30e79 --- /dev/null +++ b/crates/yaak-commands/src/actions.rs @@ -0,0 +1,157 @@ +//! The actions plugins contribute to the UI, and the calls that run them. +//! +//! Listing is a plain question for the plugin runtime. Calling is not: the +//! frontend sends back the model it was showing, and a plugin must act on what +//! that model *actually is* — re-read from the database, with inheritance +//! resolved — not on a snapshot the UI has been holding. That re-reading is the +//! work these handlers do. + +use crate::error::{Error, Result}; +use crate::host::PluginHost; +use crate::resolve::{resolve_grpc_request, resolve_http_request}; +use yaak_models::models::HttpRequest; +use yaak_plugins::events::{ + CallFolderActionArgs, CallFolderActionRequest, CallGrpcRequestActionArgs, + CallGrpcRequestActionRequest, CallHttpRequestActionArgs, CallHttpRequestActionRequest, + CallWebsocketRequestActionArgs, CallWebsocketRequestActionRequest, CallWorkspaceActionArgs, + CallWorkspaceActionRequest, GetFolderActionsResponse, GetGrpcRequestActionsResponse, + GetHttpRequestActionsResponse, GetWebsocketRequestActionsResponse, GetWorkspaceActionsResponse, +}; +use yaak_rpc_schema::*; + +// -- Listing -- + +pub async fn cmd_http_request_actions( + host: H, + _req: CmdHttpRequestActionsReq, +) -> Result> { + host.http_request_actions().await +} + +pub async fn cmd_websocket_request_actions( + host: H, + _req: CmdWebsocketRequestActionsReq, +) -> Result> { + host.websocket_request_actions().await +} + +pub async fn cmd_grpc_request_actions( + host: H, + _req: CmdGrpcRequestActionsReq, +) -> Result> { + host.grpc_request_actions().await +} + +pub async fn cmd_workspace_actions( + host: H, + _req: CmdWorkspaceActionsReq, +) -> Result> { + host.workspace_actions().await +} + +pub async fn cmd_folder_actions( + host: H, + _req: CmdFolderActionsReq, +) -> Result> { + host.folder_actions().await +} + +// -- Calling -- + +pub async fn cmd_call_http_request_action( + host: H, + req: CmdCallHttpRequestActionReq, +) -> Result<()> { + let inner = req.req; + let http_request = resolve_http_request(&host.db(), &inner.args.http_request)?.0; + host.call_http_request_action(CallHttpRequestActionRequest { + args: CallHttpRequestActionArgs { http_request }, + ..inner + }) + .await +} + +pub async fn cmd_call_grpc_request_action( + host: H, + req: CmdCallGrpcRequestActionReq, +) -> Result<()> { + let inner = req.req; + let grpc_request = resolve_grpc_request(&host.db(), &inner.args.grpc_request)?.0; + host.call_grpc_request_action(CallGrpcRequestActionRequest { + args: CallGrpcRequestActionArgs { grpc_request, ..inner.args }, + ..inner + }) + .await +} + +pub async fn cmd_call_websocket_request_action( + host: H, + req: CmdCallWebsocketRequestActionReq, +) -> Result<()> { + let inner = req.req; + let websocket_request = host.db().get_websocket_request(&inner.args.websocket_request.id)?; + host.call_websocket_request_action(CallWebsocketRequestActionRequest { + args: CallWebsocketRequestActionArgs { websocket_request }, + ..inner + }) + .await +} + +pub async fn cmd_call_workspace_action( + host: H, + req: CmdCallWorkspaceActionReq, +) -> Result<()> { + let inner = req.req; + let workspace = host.db().get_workspace(&inner.args.workspace.id)?; + host.call_workspace_action(CallWorkspaceActionRequest { + args: CallWorkspaceActionArgs { workspace }, + ..inner + }) + .await +} + +pub async fn cmd_call_folder_action( + host: H, + req: CmdCallFolderActionReq, +) -> Result<()> { + let inner = req.req; + let folder = host.db().get_folder(&inner.args.folder.id)?; + host.call_folder_action(CallFolderActionRequest { + args: CallFolderActionArgs { folder }, + ..inner + }) + .await +} + +// -- Other things the plugin runtime does -- + +/// Turn a `curl` command line into an unsaved request, by handing it to the +/// same importer plugins that read files. +pub async fn cmd_curl_to_request( + host: H, + req: CmdCurlToRequestReq, +) -> Result { + let imported = host.import_data(&req.command).await?; + + let request = imported + .resources + .http_requests + .first() + .ok_or_else(|| Error::Generic("No curl command found".to_string()))?; + + // Belongs to the workspace the user is importing into, and is not saved + // until they say so — hence the blank id. + let mut request = request.clone(); + request.workspace_id = req.workspace_id; + request.id = String::new(); + Ok(request) +} + +/// Restart every plugin, returning whatever failed to come back up. +pub async fn cmd_reload_plugins( + host: H, + _req: CmdReloadPluginsReq, +) -> Result> { + let plugins = host.db().list_plugins()?; + Ok(host.reload_plugins(plugins).await) +} diff --git a/crates/yaak-commands/src/auth.rs b/crates/yaak-commands/src/auth.rs new file mode 100644 index 00000000..b171c43f --- /dev/null +++ b/crates/yaak-commands/src/auth.rs @@ -0,0 +1,102 @@ +//! Authentication config forms and their actions. +//! +//! Both commands here do the same preparation: the frontend sends the model +//! whose auth is being edited plus the values currently in the form, and those +//! values may contain templates. They have to be rendered against the model's +//! own environment chain before a plugin sees them, or an auth plugin receives +//! `${[ api_key ]}` where it expected a key. + +use crate::error::{Error, Result}; +use crate::host::PluginHost; +use crate::render::render_json_value; +use std::collections::HashMap; +use yaak_models::models::AnyModel; +use yaak_plugins::events::{ + GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse, JsonPrimitive, + RenderPurpose, +}; +use yaak_rpc_schema::*; +use yaak_templates::RenderOptions; + +pub async fn cmd_get_http_authentication_summaries( + host: H, + _req: CmdGetHttpAuthenticationSummariesReq, +) -> Result> { + host.http_authentication_summaries().await +} + +pub async fn cmd_get_http_authentication_config( + host: H, + req: CmdGetHttpAuthenticationConfigReq, +) -> Result { + // A config form is being displayed, so a template that cannot resolve + // should show as blank rather than refuse to open the form. + let values = render_auth_values( + &host, + &req.model, + req.environment_id.as_deref(), + req.values, + RenderPurpose::Preview, + &RenderOptions::return_empty(), + ) + .await?; + + host.http_authentication_config(&req.auth_name, values, req.model.id()).await +} + +pub async fn cmd_call_http_authentication_action( + host: H, + req: CmdCallHttpAuthenticationActionReq, +) -> Result<()> { + // An action actually uses these values, so an unresolvable template is an + // error rather than an empty string that would silently authenticate wrong. + let values = render_auth_values( + &host, + &req.model, + req.environment_id.as_deref(), + req.values, + RenderPurpose::Send, + &RenderOptions::throw(), + ) + .await?; + + host.call_http_authentication_action(&req.auth_name, req.action_index, values, req.model.id()) + .await +} + +/// Render the form's values against the environment chain the model sits in. +/// +/// The chain depends on where the model lives — a request inherits through its +/// folder, a workspace has only its own — so the model is what decides which +/// variables are in scope. +async fn render_auth_values( + host: &H, + model: &AnyModel, + environment_id: Option<&str>, + values: HashMap, + purpose: RenderPurpose, + options: &RenderOptions, +) -> Result> { + let (workspace_id, folder_id) = match model { + AnyModel::HttpRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()), + AnyModel::GrpcRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()), + AnyModel::WebsocketRequest(r) => (r.workspace_id.clone(), r.folder_id.clone()), + AnyModel::Folder(f) => (f.workspace_id.clone(), f.folder_id.clone()), + AnyModel::Workspace(w) => (w.id.clone(), None), + other => { + return Err(Error::Generic(format!( + "Cannot resolve authentication for a {}", + other.model() + ))); + } + }; + + let environment_chain = + host.db().resolve_environments(&workspace_id, folder_id.as_deref(), environment_id)?; + + let cb = host.template_callback(purpose); + let rendered = + render_json_value(serde_json::to_value(&values)?, environment_chain, &cb, options).await?; + + Ok(serde_json::from_value(rendered)?) +} diff --git a/crates/yaak-commands/src/host.rs b/crates/yaak-commands/src/host.rs index cc92e914..2ed259ee 100644 --- a/crates/yaak-commands/src/host.rs +++ b/crates/yaak-commands/src/host.rs @@ -23,8 +23,13 @@ use yaak_models::models::Plugin; use yaak_models::query_manager::QueryManager; use yaak_models::util::UpdateSource; use yaak_plugins::events::{ + CallFolderActionRequest, CallGrpcRequestActionRequest, CallHttpRequestActionRequest, + CallWebsocketRequestActionRequest, CallWorkspaceActionRequest, GetFolderActionsResponse, + GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, + GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, GetThemesResponse, - JsonPrimitive, PluginContext, RenderPurpose, + GetWebsocketRequestActionsResponse, GetWorkspaceActionsResponse, ImportResponse, JsonPrimitive, + PluginContext, RenderPurpose, }; use yaak_plugins::plugin_meta::PluginMetadata; use yaak_templates::TemplateCallback; @@ -135,6 +140,77 @@ pub trait PluginHost: Host { /// Themes contributed by plugins. fn themes(&self) -> impl Future>>; + // -- Actions plugins contribute to the UI -- + + fn http_request_actions( + &self, + ) -> impl Future>>; + fn websocket_request_actions( + &self, + ) -> impl Future>>; + fn grpc_request_actions( + &self, + ) -> impl Future>>; + fn workspace_actions( + &self, + ) -> impl Future>>; + fn folder_actions(&self) -> impl Future>>; + + /// Running an action. The request in each of these has already been + /// re-read and had its inheritance resolved by the handler; a host must + /// pass it through untouched. + fn call_http_request_action( + &self, + req: CallHttpRequestActionRequest, + ) -> impl Future>; + fn call_grpc_request_action( + &self, + req: CallGrpcRequestActionRequest, + ) -> impl Future>; + fn call_websocket_request_action( + &self, + req: CallWebsocketRequestActionRequest, + ) -> impl Future>; + fn call_workspace_action( + &self, + req: CallWorkspaceActionRequest, + ) -> impl Future>; + fn call_folder_action( + &self, + req: CallFolderActionRequest, + ) -> impl Future>; + + // -- Authentication -- + + fn http_authentication_summaries( + &self, + ) -> impl Future>>; + + /// The form an auth plugin wants to show. `values` arrive already rendered. + fn http_authentication_config( + &self, + auth_name: &str, + values: HashMap, + model_id: &str, + ) -> impl Future>; + + fn call_http_authentication_action( + &self, + auth_name: &str, + action_index: i32, + values: HashMap, + model_id: &str, + ) -> impl Future>; + + // -- The importers, and the runtime itself -- + + /// Hand arbitrary text to the importer plugins and take what they make of + /// it. Used for files, URLs and pasted `curl` commands alike. + fn import_data(&self, content: &str) -> impl Future>; + + /// Restart every plugin, returning `(plugin, error)` for those that failed. + fn reload_plugins(&self, plugins: Vec) -> impl Future>; + /// Re-encrypt the `secure(...)` values in a template. /// /// Whole operation rather than its pieces because the encryption is only diff --git a/crates/yaak-commands/src/lib.rs b/crates/yaak-commands/src/lib.rs index f7e03229..72563c20 100644 --- a/crates/yaak-commands/src/lib.rs +++ b/crates/yaak-commands/src/lib.rs @@ -11,6 +11,8 @@ //! host-specific types; the ones that stay behind are the ones only a desktop //! can serve (native windows, the updater, dialogs) or that still lean on it. +pub mod actions; +pub mod auth; pub mod data; pub mod encryption; pub mod error; @@ -18,6 +20,7 @@ pub mod host; pub mod models; pub mod plugins; pub mod render; +pub mod resolve; pub mod responses; pub mod templates; diff --git a/crates/yaak-commands/src/resolve.rs b/crates/yaak-commands/src/resolve.rs new file mode 100644 index 00000000..f20e9af7 --- /dev/null +++ b/crates/yaak-commands/src/resolve.rs @@ -0,0 +1,56 @@ +//! Filling in what a request inherits from its folders and workspace. +//! +//! A request stored in the database records only what is set *on it*; +//! authentication and headers can come from any ancestor. Anything that acts on +//! a request as the user sees it — sending it, handing it to a plugin — has to +//! resolve that chain first, which is why this is shared rather than living +//! next to any one caller. + +use crate::error::Result; +use yaak_models::client_db::ClientDb; +use yaak_models::models::{GrpcRequest, HttpRequest, WebsocketRequest}; + +/// The request with inherited auth and headers filled in, plus the id of the +/// model the authentication was inherited *from* — plugins key their token +/// caches on it, so it must be the ancestor's id and not the request's. +pub fn resolve_http_request(db: &ClientDb, request: &HttpRequest) -> Result<(HttpRequest, String)> { + let mut new_request = request.clone(); + + let (authentication_type, authentication, authentication_context_id) = + db.resolve_auth_for_http_request(request)?; + new_request.authentication_type = authentication_type; + new_request.authentication = authentication; + + new_request.headers = db.resolve_headers_for_http_request(request)?; + + Ok((new_request, authentication_context_id)) +} + +pub fn resolve_grpc_request(db: &ClientDb, request: &GrpcRequest) -> Result<(GrpcRequest, String)> { + let mut new_request = request.clone(); + + let (authentication_type, authentication, authentication_context_id) = + db.resolve_auth_for_grpc_request(request)?; + new_request.authentication_type = authentication_type; + new_request.authentication = authentication; + + new_request.metadata = db.resolve_metadata_for_grpc_request(request)?; + + Ok((new_request, authentication_context_id)) +} + +pub fn resolve_websocket_request( + db: &ClientDb, + request: &WebsocketRequest, +) -> Result<(WebsocketRequest, String)> { + let mut new_request = request.clone(); + + let (authentication_type, authentication, authentication_context_id) = + db.resolve_auth_for_websocket_request(request)?; + new_request.authentication_type = authentication_type; + new_request.authentication = authentication; + + new_request.headers = db.resolve_headers_for_websocket_request(request)?; + + Ok((new_request, authentication_context_id)) +} diff --git a/crates/yaak-commands/src/templates.rs b/crates/yaak-commands/src/templates.rs index d16628c5..1664e2ad 100644 --- a/crates/yaak-commands/src/templates.rs +++ b/crates/yaak-commands/src/templates.rs @@ -56,7 +56,7 @@ pub async fn cmd_template_function_config( host: H, req: CmdTemplateFunctionConfigReq, ) -> Result { - host.template_function_config(&req.function_name, req.values, &req.model.id()).await + host.template_function_config(&req.function_name, req.values, req.model.id()).await } pub async fn cmd_get_themes( diff --git a/crates/yaak-commands/tests/test_host.rs b/crates/yaak-commands/tests/test_host.rs index 8b37ee41..1bfc3995 100644 --- a/crates/yaak-commands/tests/test_host.rs +++ b/crates/yaak-commands/tests/test_host.rs @@ -8,10 +8,12 @@ //! `PluginHost` too, without one, which is only possible because that trait //! names operations rather than handing back a manager. +use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use std::sync::{Arc, Mutex}; use tempfile::TempDir; +use yaak_commands::auth::cmd_get_http_authentication_config; use yaak_commands::models::{ cmd_default_headers, cmd_get_workspace_meta, models_delete, models_upsert, models_workspace_models, @@ -25,8 +27,13 @@ use yaak_models::models::{AnyModel, Environment, EnvironmentVariable, Plugin, Wo use yaak_models::query_manager::QueryManager; use yaak_models::util::{ModelPayload, UpdateSource}; use yaak_plugins::events::{ + CallFolderActionRequest, CallGrpcRequestActionRequest, CallHttpRequestActionRequest, + CallWebsocketRequestActionRequest, CallWorkspaceActionRequest, GetFolderActionsResponse, + GetGrpcRequestActionsResponse, GetHttpAuthenticationConfigResponse, + GetHttpAuthenticationSummaryResponse, GetHttpRequestActionsResponse, GetTemplateFunctionConfigResponse, GetTemplateFunctionSummaryResponse, GetThemesResponse, - JsonPrimitive, RenderPurpose, + GetWebsocketRequestActionsResponse, GetWorkspaceActionsResponse, ImportResponse, JsonPrimitive, + RenderPurpose, }; use yaak_plugins::plugin_meta::PluginMetadata; use yaak_rpc_schema::{ @@ -162,6 +169,9 @@ async fn host_free_handlers_need_no_state() { #[derive(Clone)] struct SingleThreadedHost { inner: Rc, + /// The values the last auth-config call arrived with, so a test can check + /// they were rendered before the host ever saw them. + auth_values: Rc>>>, } impl Host for SingleThreadedHost { @@ -260,12 +270,113 @@ impl PluginHost for SingleThreadedHost { async fn themes(&self) -> yaak_commands::Result> { Ok(Vec::new()) } + + // No plugins, so nothing contributes actions and nothing can run one. + + async fn http_request_actions( + &self, + ) -> yaak_commands::Result> { + Ok(Vec::new()) + } + + async fn websocket_request_actions( + &self, + ) -> yaak_commands::Result> { + Ok(Vec::new()) + } + + async fn grpc_request_actions( + &self, + ) -> yaak_commands::Result> { + Ok(Vec::new()) + } + + async fn workspace_actions(&self) -> yaak_commands::Result> { + Ok(Vec::new()) + } + + async fn folder_actions(&self) -> yaak_commands::Result> { + Ok(Vec::new()) + } + + async fn call_http_request_action( + &self, + _req: CallHttpRequestActionRequest, + ) -> yaak_commands::Result<()> { + Err(no_plugins()) + } + + async fn call_grpc_request_action( + &self, + _req: CallGrpcRequestActionRequest, + ) -> yaak_commands::Result<()> { + Err(no_plugins()) + } + + async fn call_websocket_request_action( + &self, + _req: CallWebsocketRequestActionRequest, + ) -> yaak_commands::Result<()> { + Err(no_plugins()) + } + + async fn call_workspace_action( + &self, + _req: CallWorkspaceActionRequest, + ) -> yaak_commands::Result<()> { + Err(no_plugins()) + } + + async fn call_folder_action(&self, _req: CallFolderActionRequest) -> yaak_commands::Result<()> { + Err(no_plugins()) + } + + async fn http_authentication_summaries( + &self, + ) -> yaak_commands::Result> { + Ok(Vec::new()) + } + + async fn http_authentication_config( + &self, + _auth_name: &str, + values: HashMap, + _model_id: &str, + ) -> yaak_commands::Result { + *self.auth_values.borrow_mut() = Some(values); + Err(no_plugins()) + } + + async fn call_http_authentication_action( + &self, + _auth_name: &str, + _action_index: i32, + _values: HashMap, + _model_id: &str, + ) -> yaak_commands::Result<()> { + Err(no_plugins()) + } + + async fn import_data(&self, _content: &str) -> yaak_commands::Result { + Err(no_plugins()) + } + + async fn reload_plugins(&self, _plugins: Vec) -> Vec<(String, String)> { + Vec::new() + } +} + +fn no_plugins() -> yaak_commands::Error { + yaak_commands::Error::Generic("no plugin runtime on this host".into()) } #[tokio::test] async fn a_single_threaded_host_can_implement_the_trait() { let TestHost { inner } = TestHost::new(); - let host = SingleThreadedHost { inner: Rc::new(Arc::into_inner(inner).expect("sole owner")) }; + let host = SingleThreadedHost { + inner: Rc::new(Arc::into_inner(inner).expect("sole owner")), + auth_values: Rc::new(RefCell::new(None)), + }; let workspace = Workspace { name: "From one thread".to_string(), ..Default::default() }; let id = models_upsert(host.clone(), ModelsUpsertReq { model: AnyModel::Workspace(workspace) }) @@ -293,7 +404,6 @@ async fn a_single_threaded_host_can_implement_the_trait() { &Environment { workspace_id: id.clone(), name: "Test env".to_string(), - base: true, variables: vec![EnvironmentVariable { enabled: true, name: "greeting".to_string(), @@ -328,3 +438,64 @@ async fn a_single_threaded_host_can_implement_the_trait() { .expect("delete"); assert_eq!(deleted, id); } + +/// Auth form values may contain templates, and a plugin must never see one +/// unrendered. The rendering happens in the shared handler, so this checks the +/// host received a resolved value rather than `${[ ... ]}`. +#[tokio::test] +async fn auth_values_are_rendered_before_the_host_sees_them() { + let TestHost { inner } = TestHost::new(); + let host = SingleThreadedHost { + inner: Rc::new(Arc::into_inner(inner).expect("sole owner")), + auth_values: Rc::new(RefCell::new(None)), + }; + + let workspace = host + .db() + .upsert_workspace( + &Workspace { name: "Auth".to_string(), ..Default::default() }, + &host.update_source(), + ) + .expect("workspace"); + host.db() + .upsert_environment( + &Environment { + workspace_id: workspace.id.clone(), + name: "Env".to_string(), + variables: vec![EnvironmentVariable { + enabled: true, + name: "token".to_string(), + value: "s3cret".to_string(), + id: None, + }], + ..Default::default() + }, + &host.update_source(), + ) + .expect("environment"); + let environment = + host.db().list_environments_ensure_base(&workspace.id).expect("list").remove(0); + + let mut values = HashMap::new(); + values.insert("password".to_string(), JsonPrimitive::String("${[ token ]}".to_string())); + + // The host refuses the call itself — it has no plugins — but only after the + // handler has rendered and handed over the values, which is what matters. + let _ = cmd_get_http_authentication_config( + host.clone(), + yaak_rpc_schema::CmdGetHttpAuthenticationConfigReq { + auth_name: "basic".to_string(), + values, + model: AnyModel::Workspace(workspace), + environment_id: Some(environment.id), + }, + ) + .await; + + let seen = host.auth_values.borrow().clone().expect("the host should have been called"); + assert!( + matches!(seen.get("password"), Some(JsonPrimitive::String(v)) if v == "s3cret"), + "the template should have been rendered before reaching the host, got {:?}", + seen.get("password"), + ); +}