Auth plugins (#155)

This commit is contained in:
Gregory Schier
2025-01-17 05:53:03 -08:00
committed by GitHub
parent e21df98a30
commit bd322162c8
56 changed files with 5468 additions and 1474 deletions

View File

@@ -1,10 +1,8 @@
use crate::render::render_http_request;
use crate::response_err;
use crate::template_callback::PluginTemplateCallback;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue};
use http::{HeaderMap, HeaderName, HeaderValue, Uri};
use log::{debug, error, warn};
use mime_guess::Mime;
use reqwest::redirect::Policy;
@@ -32,7 +30,8 @@ use yaak_models::queries::{
get_base_environment, get_http_response, get_or_create_settings, get_workspace,
update_response_if_id, upsert_cookie_jar, UpdateSource,
};
use yaak_plugins::events::{RenderPurpose, WindowContext};
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose, WindowContext};
use yaak_plugins::manager::PluginManager;
pub async fn send_http_request<R: Runtime>(
window: &WebviewWindow<R>,
@@ -42,6 +41,7 @@ pub async fn send_http_request<R: Runtime>(
cookie_jar: Option<CookieJar>,
cancelled_rx: &mut Receiver<bool>,
) -> Result<HttpResponse, String> {
let plugin_manager = window.state::<PluginManager>();
let workspace =
get_workspace(window, &request.workspace_id).await.expect("Failed to get Workspace");
let base_environment = get_base_environment(window, &request.workspace_id)
@@ -160,7 +160,7 @@ pub async fn send_http_request<R: Runtime>(
query_params.push((p.name, p.value));
}
let uri = match http::Uri::from_str(url_string.as_str()) {
let uri = match Uri::from_str(url_string.as_str()) {
Ok(u) => u,
Err(e) => {
return Ok(response_err(
@@ -234,29 +234,6 @@ pub async fn send_http_request<R: Runtime>(
headers.insert(header_name, header_value);
}
if let Some(b) = &rendered_request.authentication_type {
let empty_value = &serde_json::to_value("").unwrap();
let a = rendered_request.authentication;
if b == "basic" {
let username = a.get("username").unwrap_or(empty_value).as_str().unwrap_or_default();
let password = a.get("password").unwrap_or(empty_value).as_str().unwrap_or_default();
let auth = format!("{username}:{password}");
let encoded = BASE64_STANDARD.encode(auth);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
);
} else if b == "bearer" {
let token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or_default();
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
);
}
}
let request_body = rendered_request.body;
if let Some(body_type) = &rendered_request.body_type {
if body_type == "graphql" {
@@ -383,7 +360,7 @@ pub async fn send_http_request<R: Runtime>(
// Add headers last, because previous steps may modify them
request_builder = request_builder.headers(headers);
let sendable_req = match request_builder.build() {
let mut sendable_req = match request_builder.build() {
Ok(r) => r,
Err(e) => {
warn!("Failed to build request builder {e:?}");
@@ -391,6 +368,56 @@ pub async fn send_http_request<R: Runtime>(
}
};
// Apply authentication
// Map legacy auth name values from before they were plugins
let auth_plugin_name = match request.authentication_type.clone() {
Some(s) if s == "basic" => Some("@yaakapp/auth-basic".to_string()),
Some(s) if s == "bearer" => Some("@yaakapp/auth-bearer".to_string()),
_ => request.authentication_type.to_owned(),
};
if let Some(plugin_name) = auth_plugin_name {
let req = CallHttpAuthenticationRequest {
config: serde_json::to_value(&request.authentication)
.unwrap()
.as_object()
.unwrap()
.to_owned(),
method: sendable_req.method().to_string(),
url: sendable_req.url().to_string(),
headers: sendable_req
.headers()
.iter()
.map(|(name, value)| HttpHeader {
name: name.to_string(),
value: value.to_str().unwrap_or_default().to_string(),
})
.collect(),
};
let plugin_result =
match plugin_manager.call_http_authentication(window, &plugin_name, req).await {
Ok(r) => r,
Err(e) => {
return Ok(response_err(&*response.lock().await, e.to_string(), window).await);
}
};
{
let url = sendable_req.url_mut();
*url = Url::parse(&plugin_result.url).unwrap();
}
{
let headers = sendable_req.headers_mut();
for header in plugin_result.headers {
headers.insert(
HeaderName::from_str(&header.name).unwrap(),
HeaderValue::from_str(&header.value).unwrap(),
);
}
};
}
let (resp_tx, resp_rx) = oneshot::channel::<Result<Response, reqwest::Error>>();
let (done_tx, done_rx) = oneshot::channel::<HttpResponse>();

View File

@@ -9,8 +9,6 @@ use crate::render::{render_grpc_request, render_http_request, render_json_value,
use crate::template_callback::PluginTemplateCallback;
use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use chrono::Utc;
use eventsource_client::{EventParser, SSE};
use log::{debug, error, info, warn};
@@ -65,11 +63,11 @@ use yaak_models::queries::{
upsert_workspace_meta, BatchUpsertResult, UpdateSource,
};
use yaak_plugins::events::{
BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, Icon,
InternalEvent, InternalEventPayload, PromptTextResponse, RenderHttpRequestResponse,
RenderPurpose, SendHttpRequestResponse, ShowToastRequest, TemplateRenderResponse,
WindowContext,
BootResponse, CallHttpAuthenticationRequest, CallHttpRequestActionRequest, FilterResponse,
FindHttpResponsesResponse, GetHttpAuthenticationResponse, GetHttpRequestActionsResponse,
GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, HttpHeader, Icon, InternalEvent,
InternalEventPayload, PromptTextResponse, RenderHttpRequestResponse, RenderPurpose,
SendHttpRequestResponse, ShowToastRequest, TemplateRenderResponse, WindowContext,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
@@ -154,7 +152,7 @@ async fn cmd_render_template<R: Runtime>(
RenderPurpose::Preview,
),
)
.await;
.await;
Ok(rendered)
}
@@ -198,6 +196,7 @@ async fn cmd_grpc_go<R: Runtime>(
environment_id: Option<&str>,
proto_files: Vec<String>,
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
grpc_handle: State<'_, Mutex<GrpcHandle>>,
) -> Result<String, String> {
let environment = match environment_id {
@@ -210,7 +209,7 @@ async fn cmd_grpc_go<R: Runtime>(
.ok_or("Failed to find GRPC request")?;
let base_environment =
get_base_environment(&window, &req.workspace_id).await.map_err(|e| e.to_string())?;
let req = render_grpc_request(
let mut req = render_grpc_request(
&req,
&base_environment,
environment.as_ref(),
@@ -220,7 +219,7 @@ async fn cmd_grpc_go<R: Runtime>(
RenderPurpose::Send,
),
)
.await;
.await;
let mut metadata = BTreeMap::new();
// Add the rest of metadata
@@ -236,21 +235,37 @@ async fn cmd_grpc_go<R: Runtime>(
metadata.insert(h.name, h.value);
}
if let Some(b) = &req.authentication_type {
let req = req.clone();
let empty_value = &serde_json::to_value("").unwrap();
let a = req.authentication;
// Map legacy auth name values from before they were plugins
let auth_plugin_name = match req.authentication_type.clone() {
Some(s) if s == "basic" => Some("@yaakapp/auth-basic".to_string()),
Some(s) if s == "bearer" => Some("@yaakapp/auth-bearer".to_string()),
_ => req.authentication_type.to_owned(),
};
if let Some(plugin_name) = auth_plugin_name {
let plugin_req = CallHttpAuthenticationRequest {
config: serde_json::to_value(&req.authentication)
.unwrap()
.as_object()
.unwrap()
.to_owned(),
method: "POST".to_string(),
url: req.url.clone(),
headers: metadata
.iter()
.map(|(name, value)| HttpHeader {
name: name.to_string(),
value: value.to_string(),
})
.collect(),
};
let plugin_result = plugin_manager
.call_http_authentication(&window, &plugin_name, plugin_req)
.await
.map_err(|e| e.to_string())?;
if b == "basic" {
let username = a.get("username").unwrap_or(empty_value).as_str().unwrap_or("");
let password = a.get("password").unwrap_or(empty_value).as_str().unwrap_or("");
let auth = format!("{username}:{password}");
let encoded = BASE64_STANDARD.encode(auth);
metadata.insert("Authorization".to_string(), format!("Basic {}", encoded));
} else if b == "bearer" {
let token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
metadata.insert("Authorization".to_string(), format!("Bearer {token}"));
req.url = plugin_result.url;
for header in plugin_result.headers {
metadata.insert(header.name, header.value);
}
}
@@ -269,8 +284,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?
};
let conn_id = conn.id.clone();
@@ -322,8 +337,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())?;
.await
.map_err(|e| e.to_string())?;
return Ok(conn_id);
}
};
@@ -378,7 +393,7 @@ async fn cmd_grpc_go<R: Runtime>(
RenderPurpose::Send,
),
)
.await
.await
})
})
};
@@ -396,8 +411,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
});
return;
}
@@ -413,8 +428,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
});
}
Ok(IncomingMsg::Commit) => {
@@ -446,7 +461,7 @@ async fn cmd_grpc_go<R: Runtime>(
RenderPurpose::Send,
),
)
.await;
.await;
upsert_grpc_event(
&window,
@@ -458,8 +473,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
async move {
let (maybe_stream, maybe_msg) =
@@ -497,8 +512,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
}
match maybe_msg {
@@ -512,14 +527,14 @@ async fn cmd_grpc_go<R: Runtime>(
} else {
"Received response with metadata"
}
.to_string(),
.to_string(),
event_type: GrpcEventType::Info,
..base_event.clone()
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
upsert_grpc_event(
&window,
&GrpcEvent {
@@ -529,8 +544,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
upsert_grpc_event(
&window,
&GrpcEvent {
@@ -541,8 +556,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
}
Some(Err(e)) => {
upsert_grpc_event(
@@ -566,8 +581,8 @@ async fn cmd_grpc_go<R: Runtime>(
}),
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
}
None => {
// Server streaming doesn't return the initial message
@@ -585,14 +600,14 @@ async fn cmd_grpc_go<R: Runtime>(
} else {
"Received response with metadata"
}
.to_string(),
.to_string(),
event_type: GrpcEventType::Info,
..base_event.clone()
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
stream.into_inner()
}
Some(Err(e)) => {
@@ -618,8 +633,8 @@ async fn cmd_grpc_go<R: Runtime>(
}),
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
return;
}
None => return,
@@ -638,8 +653,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
}
Ok(None) => {
let trailers =
@@ -655,8 +670,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
break;
}
Err(status) => {
@@ -671,8 +686,8 @@ async fn cmd_grpc_go<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.unwrap();
.await
.unwrap();
}
}
}
@@ -930,8 +945,8 @@ async fn cmd_import_data<R: Runtime>(
grpc_requests,
&UpdateSource::Import,
)
.await
.map_err(|e| e.to_string())?;
.await
.map_err(|e| e.to_string())?;
analytics::track_event(
&window,
@@ -939,7 +954,7 @@ async fn cmd_import_data<R: Runtime>(
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
)
.await;
.await;
Ok(upserted)
}
@@ -960,6 +975,14 @@ async fn cmd_template_functions<R: Runtime>(
plugin_manager.get_template_functions(&window).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_get_http_authentication<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<GetHttpAuthenticationResponse>, String> {
plugin_manager.get_http_authentication(&window).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn cmd_call_http_request_action<R: Runtime>(
window: WebviewWindow<R>,
@@ -985,7 +1008,7 @@ async fn cmd_curl_to_request<R: Runtime>(
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
)
.await;
.await;
import_result.resources.http_requests.get(0).ok_or("No curl command found".to_string()).map(
|r| {
@@ -1170,8 +1193,8 @@ async fn cmd_install_plugin<R: Runtime>(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())?;
.await
.map_err(|e| e.to_string())?;
Ok(plugin)
}
@@ -1222,8 +1245,8 @@ async fn cmd_create_cookie_jar(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1245,8 +1268,8 @@ async fn cmd_create_environment(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1268,8 +1291,8 @@ async fn cmd_create_grpc_request(
},
&UpdateSource::Window,
)
.await
.map_err(|e| e.to_string())
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1508,8 +1531,8 @@ async fn cmd_list_cookie_jars(
},
&UpdateSource::Window,
)
.await
.expect("Failed to create CookieJar");
.await
.expect("Failed to create CookieJar");
Ok(vec![cookie_jar])
} else {
Ok(cookie_jars)
@@ -1598,8 +1621,8 @@ async fn cmd_list_workspaces(window: WebviewWindow) -> Result<Vec<Workspace>, St
},
&UpdateSource::Window,
)
.await
.expect("Failed to create Workspace");
.await
.expect("Failed to create Workspace");
Ok(vec![workspace])
} else {
Ok(workspaces)
@@ -1853,6 +1876,7 @@ pub fn run() {
cmd_get_environment,
cmd_get_folder,
cmd_get_grpc_request,
cmd_get_http_authentication,
cmd_get_http_request,
cmd_get_key_value,
cmd_get_settings,
@@ -1989,7 +2013,7 @@ fn create_main_window(handle: &AppHandle, url: &str) -> WebviewWindow {
Some(_) => counter += 1,
}
}
.expect("Failed to generate label for new window");
.expect("Failed to generate label for new window");
let config = CreateWindowConfig {
url,
@@ -2224,8 +2248,8 @@ async fn handle_plugin_event<R: Runtime>(
req.request_id.as_str(),
req.limit.map(|l| l as i64),
)
.await
.unwrap_or_default();
.await
.unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
}))
@@ -2254,7 +2278,7 @@ async fn handle_plugin_event<R: Runtime>(
environment.as_ref(),
&cb,
)
.await;
.await;
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request,
}))
@@ -2275,7 +2299,7 @@ async fn handle_plugin_event<R: Runtime>(
render_json_value(req.data, &base_environment, environment.as_ref(), &cb).await;
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
}
InternalEventPayload::ReloadResponse => {
InternalEventPayload::ReloadResponse(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let plugins = list_plugins(app_handle).await.unwrap();
@@ -2313,8 +2337,8 @@ async fn handle_plugin_event<R: Runtime>(
req.http_request.id.as_str(),
&UpdateSource::Plugin,
)
.await
.unwrap();
.await
.unwrap();
let result = send_http_request(
&window,
@@ -2324,7 +2348,7 @@ async fn handle_plugin_event<R: Runtime>(
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
)
.await;
.await;
let http_response = match result {
Ok(r) => r,

View File

@@ -312,7 +312,7 @@ mod placeholder_tests {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
@@ -326,7 +326,7 @@ mod placeholder_tests {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
@@ -340,7 +340,7 @@ mod placeholder_tests {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
@@ -354,7 +354,7 @@ mod placeholder_tests {
enabled: true,
name: "".to_string(),
value: "".to_string(),
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:missing"),
@@ -368,7 +368,7 @@ mod placeholder_tests {
enabled: false,
name: ":foo".to_string(),
value: "xxx".to_string(),
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
@@ -382,7 +382,7 @@ mod placeholder_tests {
name: ":foo".into(),
value: "xxx".into(),
enabled: true,
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foooo"),
@@ -396,7 +396,7 @@ mod placeholder_tests {
name: ":foo".into(),
value: "Hello World".into(),
enabled: true,
id: "p1".into(),
id: None,
};
assert_eq!(
replace_path_placeholder(&p, "https://example.com/:foo"),
@@ -413,13 +413,13 @@ mod placeholder_tests {
name: "b".to_string(),
value: "bbb".to_string(),
enabled: true,
id: "p1".into(),
id: None,
},
HttpUrlParameter {
name: ":a".to_string(),
value: "aaa".to_string(),
enabled: true,
id: "p2".into(),
id: None,
},
],
..Default::default()

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use tauri::{AppHandle, Manager, Runtime};
use yaak_plugins::events::{RenderPurpose, TemplateFunctionArg, WindowContext};
use yaak_plugins::events::{FormInput, RenderPurpose, WindowContext};
use yaak_plugins::manager::PluginManager;
use yaak_templates::TemplateCallback;
@@ -48,11 +48,11 @@ impl TemplateCallback for PluginTemplateCallback {
// Fill in default values for all args
for a_def in function.args {
let base = match a_def {
TemplateFunctionArg::Text(a) => a.base,
TemplateFunctionArg::Select(a) => a.base,
TemplateFunctionArg::Checkbox(a) => a.base,
TemplateFunctionArg::File(a) => a.base,
TemplateFunctionArg::HttpRequest(a) => a.base,
FormInput::Text(a) => a.base,
FormInput::Select(a) => a.base,
FormInput::Checkbox(a) => a.base,
FormInput::File(a) => a.base,
FormInput::HttpRequest(a) => a.base,
};
if let None = args_with_defaults.get(base.name.as_str()) {
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());