mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-19 23:41:18 +02:00
Decouple core Yaak logic from Tauri (#354)
This commit is contained in:
259
crates/yaak-plugins/src/native_template_functions.rs
Normal file
259
crates/yaak-plugins/src/native_template_functions.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
//! Native template functions implemented in Rust.
|
||||
//!
|
||||
//! These are built-in template functions that don't require plugins:
|
||||
//! - `secure()` - encrypts/decrypts values using the EncryptionManager
|
||||
//! - `keychain()` / `keyring()` - accesses system keychain
|
||||
|
||||
use crate::events::{
|
||||
Color, FormInput, FormInputBanner, FormInputBase, FormInputMarkdown, FormInputText,
|
||||
PluginContext, RenderPurpose, TemplateFunction, TemplateFunctionArg,
|
||||
TemplateFunctionPreviewType,
|
||||
};
|
||||
use crate::manager::PluginManager;
|
||||
use crate::template_callback::PluginTemplateCallback;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use keyring::Error::NoEntry;
|
||||
use log::{debug, info};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use yaak_common::platform::{OperatingSystem, get_os};
|
||||
use yaak_crypto::manager::EncryptionManager;
|
||||
use yaak_templates::error::Error::RenderError;
|
||||
use yaak_templates::error::Result;
|
||||
use yaak_templates::{FnArg, Parser, Token, Tokens, Val, transform_args};
|
||||
|
||||
pub(crate) fn template_function_secure() -> TemplateFunction {
|
||||
TemplateFunction {
|
||||
name: "secure".to_string(),
|
||||
preview_type: Some(TemplateFunctionPreviewType::None),
|
||||
description: Some("Securely store encrypted text".to_string()),
|
||||
aliases: None,
|
||||
preview_args: None,
|
||||
args: vec![TemplateFunctionArg::FormInput(FormInput::Text(
|
||||
FormInputText {
|
||||
multi_line: Some(true),
|
||||
password: Some(true),
|
||||
base: FormInputBase {
|
||||
name: "value".to_string(),
|
||||
label: Some("Value".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
))],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn template_function_keyring() -> TemplateFunction {
|
||||
struct Meta {
|
||||
description: String,
|
||||
service_label: String,
|
||||
account_label: String,
|
||||
}
|
||||
|
||||
let meta = match get_os() {
|
||||
OperatingSystem::MacOS => Meta {
|
||||
description:
|
||||
"Access application passwords from the macOS Login keychain".to_string(),
|
||||
service_label: "Where".to_string(),
|
||||
account_label: "Account".to_string(),
|
||||
},
|
||||
OperatingSystem::Windows => Meta {
|
||||
description: "Access a secret via Windows Credential Manager".to_string(),
|
||||
service_label: "Target".to_string(),
|
||||
account_label: "Username".to_string(),
|
||||
},
|
||||
_ => Meta {
|
||||
description: "Access a secret via [Secret Service](https://specifications.freedesktop.org/secret-service/latest/) (eg. Gnome keyring or KWallet)".to_string(),
|
||||
service_label: "Collection".to_string(),
|
||||
account_label: "Account".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
TemplateFunction {
|
||||
name: "keychain".to_string(),
|
||||
preview_type: Some(TemplateFunctionPreviewType::Live),
|
||||
description: Some(meta.description),
|
||||
aliases: Some(vec!["keyring".to_string()]),
|
||||
preview_args: Some(vec!["service".to_string(), "account".to_string()]),
|
||||
args: vec template function, which encrypts values using a key stored in keychain".to_string(),
|
||||
hidden: None,
|
||||
})]),
|
||||
color: Some(Color::Info),
|
||||
hidden: None,
|
||||
})),
|
||||
TemplateFunctionArg::FormInput(FormInput::Text(FormInputText {
|
||||
base: FormInputBase {
|
||||
name: "service".to_string(),
|
||||
label: Some(meta.service_label),
|
||||
description: Some("App or URL for the password".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})),
|
||||
TemplateFunctionArg::FormInput(FormInput::Text(FormInputText {
|
||||
base: FormInputBase {
|
||||
name: "account".to_string(),
|
||||
label: Some(meta.account_label),
|
||||
description: Some("Username or email address".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn template_function_secure_run(
|
||||
encryption_manager: &EncryptionManager,
|
||||
args: HashMap<String, serde_json::Value>,
|
||||
plugin_context: &PluginContext,
|
||||
) -> Result<String> {
|
||||
match plugin_context.workspace_id.clone() {
|
||||
Some(wid) => {
|
||||
let value = args.get("value").map(|v| v.to_owned()).unwrap_or_default();
|
||||
let value = match value {
|
||||
serde_json::Value::String(s) => s,
|
||||
_ => return Ok("".to_string()),
|
||||
};
|
||||
|
||||
if value.is_empty() {
|
||||
return Ok("".to_string());
|
||||
}
|
||||
|
||||
let value = match value.strip_prefix("YENC_") {
|
||||
None => {
|
||||
return Err(RenderError("Could not decrypt non-encrypted value".to_string()));
|
||||
}
|
||||
Some(v) => v,
|
||||
};
|
||||
|
||||
let value = BASE64_STANDARD.decode(&value).unwrap();
|
||||
let r = match encryption_manager.decrypt(&wid, value.as_slice()) {
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) => Err(RenderError(e.to_string())),
|
||||
}?;
|
||||
let r = String::from_utf8(r).map_err(|e| RenderError(e.to_string()))?;
|
||||
Ok(r)
|
||||
}
|
||||
_ => Err(RenderError("workspace_id missing from plugin context".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn template_function_secure_transform_arg(
|
||||
encryption_manager: &EncryptionManager,
|
||||
plugin_context: &PluginContext,
|
||||
arg_name: &str,
|
||||
arg_value: &str,
|
||||
) -> Result<String> {
|
||||
if arg_name != "value" {
|
||||
return Ok(arg_value.to_string());
|
||||
}
|
||||
|
||||
match plugin_context.workspace_id.clone() {
|
||||
Some(wid) => {
|
||||
if arg_value.is_empty() {
|
||||
return Ok("".to_string());
|
||||
}
|
||||
|
||||
if arg_value.starts_with("YENC_") {
|
||||
// Already encrypted, so do nothing
|
||||
return Ok(arg_value.to_string());
|
||||
}
|
||||
|
||||
let r = encryption_manager
|
||||
.encrypt(&wid, arg_value.as_bytes())
|
||||
.map_err(|e| RenderError(e.to_string()))?;
|
||||
let r = BASE64_STANDARD.encode(r);
|
||||
Ok(format!("YENC_{}", r))
|
||||
}
|
||||
_ => Err(RenderError("workspace_id missing from plugin context".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt_secure_template_function(
|
||||
encryption_manager: &EncryptionManager,
|
||||
plugin_context: &PluginContext,
|
||||
template: &str,
|
||||
) -> Result<String> {
|
||||
let mut parsed = Parser::new(template).parse()?;
|
||||
let mut new_tokens: Vec<Token> = Vec::new();
|
||||
|
||||
for token in parsed.tokens.iter() {
|
||||
match token {
|
||||
Token::Tag { val: Val::Fn { name, args } } if name == "secure" => {
|
||||
let mut args_map = HashMap::new();
|
||||
for a in args {
|
||||
match a.clone().value {
|
||||
Val::Str { text } => {
|
||||
args_map.insert(a.name.to_string(), serde_json::Value::String(text));
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
new_tokens.push(Token::Raw {
|
||||
text: template_function_secure_run(encryption_manager, args_map, plugin_context)?,
|
||||
});
|
||||
}
|
||||
t => {
|
||||
new_tokens.push(t.clone());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
parsed.tokens = new_tokens;
|
||||
Ok(parsed.to_string())
|
||||
}
|
||||
|
||||
pub fn encrypt_secure_template_function(
|
||||
plugin_manager: Arc<PluginManager>,
|
||||
encryption_manager: Arc<EncryptionManager>,
|
||||
plugin_context: &PluginContext,
|
||||
template: &str,
|
||||
) -> Result<String> {
|
||||
let decrypted = decrypt_secure_template_function(&encryption_manager, plugin_context, template)?;
|
||||
let tokens = Tokens {
|
||||
tokens: vec![Token::Tag {
|
||||
val: Val::Fn {
|
||||
name: "secure".to_string(),
|
||||
args: vec![FnArg {
|
||||
name: "value".to_string(),
|
||||
value: Val::Str { text: decrypted },
|
||||
}],
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
Ok(transform_args(
|
||||
tokens,
|
||||
&PluginTemplateCallback::new(plugin_manager, encryption_manager, plugin_context, RenderPurpose::Preview),
|
||||
)?
|
||||
.to_string())
|
||||
}
|
||||
|
||||
pub fn template_function_keychain_run(args: HashMap<String, serde_json::Value>) -> Result<String> {
|
||||
let service = args.get("service").and_then(|v| v.as_str()).unwrap_or_default().to_owned();
|
||||
let user = args.get("account").and_then(|v| v.as_str()).unwrap_or_default().to_owned();
|
||||
debug!("Getting password for service {} and user {}", service, user);
|
||||
let entry = match keyring::Entry::new(&service, &user) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
debug!("Failed to initialize keyring entry for '{}' and '{}' {:?}", service, user, e);
|
||||
return Ok("".to_string()); // Don't fail for invalid args
|
||||
}
|
||||
};
|
||||
|
||||
match entry.get_password() {
|
||||
Ok(p) => Ok(p),
|
||||
Err(NoEntry) => {
|
||||
info!("No password found for '{}' and '{}'", service, user);
|
||||
Ok("".to_string()) // Don't fail for missing passwords
|
||||
}
|
||||
Err(e) => Err(RenderError(e.to_string())),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user