pub mod commands;
pub mod error;
pub mod types;
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tracing::{debug, instrument, warn};
pub use commands::OpCommand;
pub use error::OpError;
pub use types::*;
#[derive(Debug, Clone)]
pub struct OpClient {
op_path: String,
default_account: Option<String>,
}
impl OpClient {
pub fn new() -> Result<Self, OpError> {
let op_path = which::which("op")
.map_err(|_| OpError::NotFound)?
.to_string_lossy()
.to_string();
debug!(op_path = %op_path, "Found op CLI");
Ok(Self {
op_path,
default_account: None,
})
}
pub fn with_path(op_path: String) -> Self {
Self {
op_path,
default_account: None,
}
}
pub fn with_account(mut self, account: Option<String>) -> Self {
self.default_account = account;
self
}
#[instrument(skip(self), fields(op_path = %self.op_path))]
async fn execute(&self, mut cmd: OpCommand) -> Result<String, OpError> {
if self.default_account.is_some() {
cmd = cmd.account(self.default_account.as_deref());
}
let mut command = cmd.build(&self.op_path);
command.stdout(Stdio::piped()).stderr(Stdio::piped());
debug!(?command, "Executing op command");
let output = Command::from(command).output().await?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if !output.status.success() {
warn!(
status = ?output.status,
stderr = %stderr,
"op command failed"
);
return Err(OpError::from_stderr(&stderr));
}
debug!(stdout_len = stdout.len(), "op command succeeded");
Ok(stdout)
}
#[instrument(skip(self, input), fields(op_path = %self.op_path))]
async fn execute_with_stdin(&self, mut cmd: OpCommand, input: &str) -> Result<String, OpError> {
if self.default_account.is_some() {
cmd = cmd.account(self.default_account.as_deref());
}
let mut command = cmd.build(&self.op_path);
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
debug!(?command, "Executing op command with stdin");
let mut child = Command::from(command).spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(input.as_bytes()).await?;
}
let output = child.wait_with_output().await?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if !output.status.success() {
warn!(
status = ?output.status,
stderr = %stderr,
"op command failed"
);
return Err(OpError::from_stderr(&stderr));
}
debug!(stdout_len = stdout.len(), "op command succeeded");
Ok(stdout)
}
async fn execute_json<T: serde::de::DeserializeOwned>(
&self,
cmd: OpCommand,
) -> Result<T, OpError> {
let output = self.execute(cmd).await?;
serde_json::from_str(&output).map_err(|e| OpError::ParseError(e.to_string()))
}
#[instrument(skip(self))]
pub async fn whoami(&self) -> Result<WhoAmI, OpError> {
self.execute_json(OpCommand::whoami()).await
}
#[instrument(skip(self))]
pub async fn signin(&self, account: Option<&str>) -> Result<String, OpError> {
let cmd = OpCommand::signin().account(account);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn signout(&self, account: Option<&str>, all: bool, forget: bool) -> Result<String, OpError> {
let cmd = OpCommand::signout()
.account(account)
.flag_if("all", all)
.flag_if("forget", forget);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn account_list(&self) -> Result<Vec<Account>, OpError> {
self.execute_json(OpCommand::account_list()).await
}
#[instrument(skip(self))]
pub async fn account_get(&self, account: Option<&str>) -> Result<AccountDetails, OpError> {
self.execute_json(OpCommand::account_get(account)).await
}
#[instrument(skip(self))]
pub async fn account_add(
&self,
address: &str,
email: &str,
shorthand: Option<&str>,
) -> Result<String, OpError> {
let cmd = OpCommand::account_add(address, email)
.option_opt("shorthand", shorthand);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn account_forget(&self, account: &str) -> Result<String, OpError> {
self.execute(OpCommand::account_forget(account)).await
}
#[instrument(skip(self))]
pub async fn vault_list(&self) -> Result<Vec<Vault>, OpError> {
self.execute_json(OpCommand::vault_list()).await
}
#[instrument(skip(self))]
pub async fn vault_get(&self, vault: &str) -> Result<Vault, OpError> {
self.execute_json(OpCommand::vault_get(vault)).await
}
#[instrument(skip(self))]
pub async fn vault_create(
&self,
name: &str,
description: Option<&str>,
icon: Option<&str>,
allow_admins_to_manage: Option<bool>,
) -> Result<Vault, OpError> {
let mut cmd = OpCommand::vault_create(name)
.option_opt("description", description)
.option_opt("icon", icon);
if let Some(allow) = allow_admins_to_manage {
cmd = cmd.option("allow-admins-to-manage", if allow { "true" } else { "false" });
}
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn vault_edit(
&self,
vault: &str,
name: Option<&str>,
description: Option<&str>,
icon: Option<&str>,
travel_mode: Option<bool>,
) -> Result<Vault, OpError> {
let cmd = OpCommand::vault_edit(vault)
.option_opt("name", name)
.option_opt("description", description)
.option_opt("icon", icon)
.flag_if("travel-mode", travel_mode.unwrap_or(false));
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn vault_delete(&self, vault: &str) -> Result<String, OpError> {
self.execute(OpCommand::vault_delete(vault)).await
}
#[instrument(skip(self))]
pub async fn vault_user_list(&self, vault: &str) -> Result<Vec<VaultUserAccess>, OpError> {
self.execute_json(OpCommand::vault_user_list(vault)).await
}
#[instrument(skip(self))]
pub async fn vault_user_grant(
&self,
vault: &str,
user: &str,
permissions: Option<&str>,
) -> Result<String, OpError> {
let cmd = OpCommand::vault_user_grant(vault, user)
.option_opt("permissions", permissions);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn vault_user_revoke(&self, vault: &str, user: &str) -> Result<String, OpError> {
self.execute(OpCommand::vault_user_revoke(vault, user)).await
}
#[instrument(skip(self))]
pub async fn vault_group_list(&self, vault: &str) -> Result<Vec<VaultGroupAccess>, OpError> {
self.execute_json(OpCommand::vault_group_list(vault)).await
}
#[instrument(skip(self))]
pub async fn vault_group_grant(
&self,
vault: &str,
group: &str,
permissions: Option<&str>,
) -> Result<String, OpError> {
let cmd = OpCommand::vault_group_grant(vault, group)
.option_opt("permissions", permissions);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn vault_group_revoke(&self, vault: &str, group: &str) -> Result<String, OpError> {
self.execute(OpCommand::vault_group_revoke(vault, group)).await
}
#[instrument(skip(self))]
pub async fn item_list(
&self,
vault: Option<&str>,
categories: Option<&[&str]>,
tags: Option<&[&str]>,
favorite: Option<bool>,
) -> Result<Vec<ItemSummary>, OpError> {
let mut cmd = OpCommand::item_list().option_opt("vault", vault);
if let Some(cats) = categories {
for cat in cats {
cmd = cmd.option("categories", cat);
}
}
if let Some(tags) = tags {
for tag in tags {
cmd = cmd.option("tags", tag);
}
}
if let Some(fav) = favorite {
cmd = cmd.flag_if("favorite", fav);
}
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn item_get(
&self,
item: &str,
vault: Option<&str>,
reveal: bool,
) -> Result<Item, OpError> {
let cmd = OpCommand::item_get(item)
.option_opt("vault", vault)
.flag_if("reveal", reveal);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn item_create(
&self,
category: &str,
title: &str,
vault: Option<&str>,
generate_password: Option<&str>,
url: Option<&str>,
tags: Option<&[&str]>,
fields: Option<&[&str]>,
favorite: bool,
) -> Result<Item, OpError> {
let mut cmd = OpCommand::item_create(category, title)
.option_opt("vault", vault)
.option_opt("url", url)
.flag_if("favorite", favorite);
if let Some(recipe) = generate_password {
if recipe == "true" {
cmd = cmd.flag("generate-password");
} else {
cmd = cmd.option("generate-password", recipe);
}
}
if let Some(tags) = tags {
cmd = cmd.option_multi("tags", Some(tags));
}
if let Some(fields) = fields {
for field in fields {
cmd = cmd.arg(field);
}
}
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn item_edit(
&self,
item: &str,
vault: Option<&str>,
title: Option<&str>,
url: Option<&str>,
generate_password: Option<&str>,
tags: Option<&[&str]>,
fields: Option<&[&str]>,
favorite: Option<bool>,
) -> Result<Item, OpError> {
let mut cmd = OpCommand::item_edit(item)
.option_opt("vault", vault)
.option_opt("title", title)
.option_opt("url", url);
if let Some(recipe) = generate_password {
if recipe == "true" {
cmd = cmd.flag("generate-password");
} else {
cmd = cmd.option("generate-password", recipe);
}
}
if let Some(fav) = favorite {
if fav {
cmd = cmd.flag("favorite");
} else {
cmd = cmd.option("favorite", "false");
}
}
if let Some(tags) = tags {
cmd = cmd.option_multi("tags", Some(tags));
}
if let Some(fields) = fields {
for field in fields {
cmd = cmd.arg(field);
}
}
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn item_delete(
&self,
item: &str,
vault: Option<&str>,
archive: bool,
) -> Result<String, OpError> {
let cmd = OpCommand::item_delete(item)
.option_opt("vault", vault)
.flag_if("archive", archive);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn item_move(
&self,
item: &str,
current_vault: Option<&str>,
destination_vault: &str,
) -> Result<String, OpError> {
let cmd = OpCommand::item_move(item, destination_vault)
.option_opt("current-vault", current_vault);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn item_share(
&self,
item: &str,
vault: Option<&str>,
expiry: Option<&str>,
emails: Option<&[&str]>,
view_once: bool,
) -> Result<String, OpError> {
let mut cmd = OpCommand::item_share(item)
.option_opt("vault", vault)
.option_opt("expiry", expiry)
.flag_if("view-once", view_once);
if let Some(emails) = emails {
cmd = cmd.option_multi("emails", Some(emails));
}
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn item_template_list(&self) -> Result<Vec<ItemTemplate>, OpError> {
self.execute_json(OpCommand::item_template_list()).await
}
#[instrument(skip(self))]
pub async fn item_template_get(&self, template: &str) -> Result<ItemTemplate, OpError> {
self.execute_json(OpCommand::item_template_get(template)).await
}
#[instrument(skip(self))]
pub async fn document_list(&self, vault: Option<&str>) -> Result<Vec<DocumentSummary>, OpError> {
let cmd = OpCommand::document_list().option_opt("vault", vault);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn document_get(
&self,
document: &str,
vault: Option<&str>,
output: Option<&str>,
) -> Result<String, OpError> {
let cmd = OpCommand::document_get(document)
.option_opt("vault", vault)
.option_opt("output", output);
let output_result = self.execute(cmd).await?;
if output.is_some() {
Ok(format!("Document saved successfully"))
} else {
use base64::Engine;
Ok(base64::engine::general_purpose::STANDARD.encode(output_result.as_bytes()))
}
}
#[instrument(skip(self))]
pub async fn document_create(
&self,
file_path: &str,
vault: Option<&str>,
title: Option<&str>,
tags: Option<&[&str]>,
) -> Result<Document, OpError> {
let cmd = OpCommand::document_create(file_path)
.option_opt("vault", vault)
.option_opt("title", title)
.option_multi("tags", tags);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn document_edit(
&self,
document: &str,
file_path: &str,
vault: Option<&str>,
title: Option<&str>,
) -> Result<Document, OpError> {
let cmd = OpCommand::document_edit(document, file_path)
.option_opt("vault", vault)
.option_opt("title", title);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn document_delete(
&self,
document: &str,
vault: Option<&str>,
archive: bool,
) -> Result<String, OpError> {
let cmd = OpCommand::document_delete(document)
.option_opt("vault", vault)
.flag_if("archive", archive);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn user_list(
&self,
group: Option<&str>,
vault: Option<&str>,
) -> Result<Vec<User>, OpError> {
let cmd = OpCommand::user_list()
.option_opt("group", group)
.option_opt("vault", vault);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn user_get(&self, user: &str) -> Result<User, OpError> {
self.execute_json(OpCommand::user_get(user)).await
}
#[instrument(skip(self))]
pub async fn user_provision(
&self,
email: &str,
name: &str,
language: Option<&str>,
) -> Result<User, OpError> {
let cmd = OpCommand::user_provision(email, name)
.option_opt("language", language);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn user_confirm(&self, user: &str) -> Result<String, OpError> {
self.execute(OpCommand::user_confirm(user)).await
}
#[instrument(skip(self))]
pub async fn user_edit(
&self,
user: &str,
name: Option<&str>,
travel_mode: Option<bool>,
) -> Result<User, OpError> {
let mut cmd = OpCommand::user_edit(user)
.option_opt("name", name);
if let Some(tm) = travel_mode {
cmd = cmd.flag_if("travel-mode", tm);
}
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn user_suspend(
&self,
user: &str,
deauthorize_devices_after: Option<i64>,
) -> Result<String, OpError> {
let mut cmd = OpCommand::user_suspend(user);
if let Some(seconds) = deauthorize_devices_after {
cmd = cmd.option("deauthorize-devices-after", &format!("{}s", seconds));
}
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn user_reactivate(&self, user: &str) -> Result<String, OpError> {
self.execute(OpCommand::user_reactivate(user)).await
}
#[instrument(skip(self))]
pub async fn user_delete(&self, user: &str) -> Result<String, OpError> {
self.execute(OpCommand::user_delete(user)).await
}
#[instrument(skip(self))]
pub async fn group_list(
&self,
user: Option<&str>,
vault: Option<&str>,
) -> Result<Vec<Group>, OpError> {
let cmd = OpCommand::group_list()
.option_opt("user", user)
.option_opt("vault", vault);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn group_get(&self, group: &str) -> Result<Group, OpError> {
self.execute_json(OpCommand::group_get(group)).await
}
#[instrument(skip(self))]
pub async fn group_create(
&self,
name: &str,
description: Option<&str>,
) -> Result<Group, OpError> {
let cmd = OpCommand::group_create(name)
.option_opt("description", description);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn group_edit(
&self,
group: &str,
name: Option<&str>,
description: Option<&str>,
) -> Result<Group, OpError> {
let cmd = OpCommand::group_edit(group)
.option_opt("name", name)
.option_opt("description", description);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn group_delete(&self, group: &str) -> Result<String, OpError> {
self.execute(OpCommand::group_delete(group)).await
}
#[instrument(skip(self))]
pub async fn group_user_list(&self, group: &str) -> Result<Vec<GroupMember>, OpError> {
self.execute_json(OpCommand::group_user_list(group)).await
}
#[instrument(skip(self))]
pub async fn group_user_grant(
&self,
group: &str,
user: &str,
role: Option<&str>,
) -> Result<String, OpError> {
let cmd = OpCommand::group_user_grant(group, user)
.option_opt("role", role);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn group_user_revoke(&self, group: &str, user: &str) -> Result<String, OpError> {
self.execute(OpCommand::group_user_revoke(group, user)).await
}
#[instrument(skip(self))]
pub async fn connect_server_list(&self) -> Result<Vec<ConnectServer>, OpError> {
self.execute_json(OpCommand::connect_server_list()).await
}
#[instrument(skip(self))]
pub async fn connect_server_get(&self, server: &str) -> Result<ConnectServer, OpError> {
self.execute_json(OpCommand::connect_server_get(server)).await
}
#[instrument(skip(self))]
pub async fn connect_server_create(
&self,
name: &str,
vaults: Option<&[&str]>,
) -> Result<ConnectServerCreateResult, OpError> {
let cmd = OpCommand::connect_server_create(name)
.option_multi("vaults", vaults);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn connect_server_edit(&self, server: &str, name: &str) -> Result<ConnectServer, OpError> {
let cmd = OpCommand::connect_server_edit(server)
.option("name", name);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn connect_server_delete(&self, server: &str) -> Result<String, OpError> {
self.execute(OpCommand::connect_server_delete(server)).await
}
#[instrument(skip(self))]
pub async fn connect_token_list(&self, server: &str) -> Result<Vec<ConnectToken>, OpError> {
self.execute_json(OpCommand::connect_token_list(server)).await
}
#[instrument(skip(self))]
pub async fn connect_token_create(
&self,
server: &str,
name: &str,
expires_in: Option<&str>,
) -> Result<String, OpError> {
let cmd = OpCommand::connect_token_create(server, name)
.option_opt("expires-in", expires_in);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn connect_token_edit(
&self,
server: &str,
token: &str,
name: &str,
) -> Result<ConnectToken, OpError> {
let cmd = OpCommand::connect_token_edit(server, token)
.option("name", name);
self.execute_json(cmd).await
}
#[instrument(skip(self))]
pub async fn connect_token_delete(&self, server: &str, token: &str) -> Result<String, OpError> {
self.execute(OpCommand::connect_token_delete(server, token)).await
}
#[instrument(skip(self))]
pub async fn connect_vault_grant(&self, server: &str, vault: &str) -> Result<String, OpError> {
self.execute(OpCommand::connect_vault_grant(server, vault)).await
}
#[instrument(skip(self))]
pub async fn connect_vault_revoke(&self, server: &str, vault: &str) -> Result<String, OpError> {
self.execute(OpCommand::connect_vault_revoke(server, vault)).await
}
#[instrument(skip(self))]
pub async fn service_account_create(
&self,
name: &str,
vaults: Option<&[&str]>,
expires_in: Option<&str>,
) -> Result<String, OpError> {
let cmd = OpCommand::service_account_create(name)
.option_multi("vaults", vaults)
.option_opt("expires-in", expires_in);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn service_account_ratelimit(&self) -> Result<RateLimitInfo, OpError> {
self.execute_json(OpCommand::service_account_ratelimit()).await
}
#[instrument(skip(self))]
pub async fn events_api_create(
&self,
name: &str,
expires_in: Option<&str>,
features: Option<&[&str]>,
) -> Result<String, OpError> {
let cmd = OpCommand::events_api_create(name)
.option_opt("expires-in", expires_in)
.option_multi("features", features);
self.execute(cmd).await
}
#[instrument(skip(self))]
pub async fn read(&self, reference: &str) -> Result<String, OpError> {
if !reference.starts_with("op://") {
return Err(OpError::InvalidSecretReference(reference.to_string()));
}
let cmd = OpCommand::read(reference);
let output = self.execute(cmd).await?;
Ok(output.trim().to_string())
}
#[instrument(skip(self))]
pub async fn inject(&self, template: &str) -> Result<String, OpError> {
let cmd = OpCommand::inject();
self.execute_with_stdin(cmd, template).await
}
#[instrument(skip(self))]
pub async fn run(
&self,
command: &str,
env: Option<&[&str]>,
env_file: Option<&str>,
no_masking: bool,
) -> Result<String, OpError> {
let mut cmd = OpCommand::run()
.option_opt("env-file", env_file)
.flag_if("no-masking", no_masking)
.arg("--")
.arg(command);
if let Some(env_vars) = env {
cmd = cmd.option_multi("env", Some(env_vars));
}
self.execute(cmd).await
}
pub async fn is_authenticated(&self) -> bool {
self.whoami().await.is_ok()
}
}
impl Default for OpClient {
fn default() -> Self {
Self::new().expect("op CLI not found")
}
}