🏡 index : ~doyle/1p.git

author Jordan Doyle <jordan@doyle.la> 2020-07-06 23:37:03.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2020-07-06 23:37:03.0 +00:00:00
commit
b42278b1abd67a941782ce1a65d94808ba4c1631 [patch]
tree
a24b2111f7de5deb73817317e2bc8d1e1416de45
parent
f881ad5ea512acbce183ad83d050529fc2c752b0
download
b42278b1abd67a941782ce1a65d94808ba4c1631.tar.gz

onep-api -> onep-backend-api & onep-api-op -> onep-backend-op



Diff

 Cargo.lock                  |  10 +-
 Cargo.toml                  |   4 +-
 onep-api-op/Cargo.toml      |  14 +--
 onep-api-op/src/lib.rs      | 301 +---------------------------------------------
 onep-api/Cargo.toml         |   9 +-
 onep-api/src/lib.rs         |  58 +---------
 onep-backend-api/Cargo.toml |   9 +-
 onep-backend-api/src/lib.rs |  58 +++++++++-
 onep-backend-op/Cargo.toml  |  14 ++-
 onep-backend-op/src/lib.rs  | 302 +++++++++++++++++++++++++++++++++++++++++++++-
 onep-cli/Cargo.toml         |   4 +-
 onep-cli/src/main.rs        |   9 +-
 12 files changed, 397 insertions(+), 395 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index cce23b2..1214001 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -173,14 +173,14 @@ version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"

[[package]]
name = "onep-api"
name = "onep-backend-api"
version = "0.1.0"

[[package]]
name = "onep-api-op"
name = "onep-backend-op"
version = "0.1.0"
dependencies = [
 "onep-api 0.1.0",
 "onep-backend-api 0.1.0",
 "serde 1.0.114 (registry+https://github.com/rust-lang/crates.io-index)",
 "serde_json 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)",
 "serde_with 1.5.0-alpha.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -195,8 +195,8 @@ dependencies = [
 "clap 3.0.0-beta.1 (git+https://github.com/clap-rs/clap)",
 "colored 1.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
 "itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "onep-api 0.1.0",
 "onep-api-op 0.1.0",
 "onep-backend-api 0.1.0",
 "onep-backend-op 0.1.0",
 "term-table 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
 "thiserror 1.0.20 (registry+https://github.com/rust-lang/crates.io-index)",
]
diff --git a/Cargo.toml b/Cargo.toml
index 675a161..c376c6e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[workspace]
members = [
    "onep-cli",
    "onep-api",
    "onep-api-op",
    "onep-backend-api",
    "onep-backend-op",
]
\ No newline at end of file
diff --git a/onep-api-op/Cargo.toml b/onep-api-op/Cargo.toml
deleted file mode 100644
index f7a4e12..0000000
--- a/onep-api-op/Cargo.toml
+++ /dev/null
@@ -1,14 +0,0 @@
[package]
name = "onep-api-op"
version = "0.1.0"
authors = ["Jordan Doyle <jordan@doyle.la>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
onep-api = { path = "../onep-api" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_with = "1.5.0-alpha.1"
thiserror = "1.0"
\ No newline at end of file
diff --git a/onep-api-op/src/lib.rs b/onep-api-op/src/lib.rs
deleted file mode 100644
index 8dda8d6..0000000
--- a/onep-api-op/src/lib.rs
+++ /dev/null
@@ -1,301 +0,0 @@
#![deny(clippy::pedantic)]

use serde::Deserialize;
use serde_json::Value;
use std::borrow::Cow;
use std::process::Command;

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("op backend returned an error:\n{0}")]
    Backend(String),
    #[error("failed to exec backend:\n{0}")]
    Exec(std::io::Error),
    #[error("failed to parse json from op:\n{0}")]
    Json(#[from] serde_json::error::Error),
    #[error("failed to convert op response to utf-8:\n{0}")]
    Utf8(#[from] std::str::Utf8Error),
}

#[derive(Debug, Deserialize)]
struct GetAccount {
    name: String,
    domain: String,
}

impl Into<onep_api::AccountMetadata> for GetAccount {
    fn into(self) -> onep_api::AccountMetadata {
        onep_api::AccountMetadata {
            name: self.name,
            domain: self.domain,
        }
    }
}

#[derive(Debug, Deserialize)]
struct ListVault {
    uuid: String,
    name: String,
}

impl Into<onep_api::VaultMetadata> for ListVault {
    fn into(self) -> onep_api::VaultMetadata {
        onep_api::VaultMetadata {
            uuid: self.uuid,
            name: self.name,
        }
    }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ListItem {
    uuid: String,
    vault_uuid: String,
    created_at: String,
    updated_at: String,
    overview: ItemOverview,
}

impl Into<onep_api::ItemMetadata> for ListItem {
    fn into(self) -> onep_api::ItemMetadata {
        onep_api::ItemMetadata {
            title: self.overview.title,
            account_info: self.overview.account_info,
            uuid: self.uuid,
            vault_uuid: self.vault_uuid,
        }
    }
}

#[derive(Debug, Deserialize)]
struct ItemOverview {
    #[serde(rename = "URLs", default)]
    urls: Vec<ItemOverviewUrl>,
    title: String,
    url: Option<String>,
    #[serde(rename = "ainfo")]
    account_info: String,
    #[serde(default)]
    tags: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct ItemOverviewUrl {
    #[serde(rename = "l")]
    label: String,
    #[serde(rename = "u")]
    url: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GetItem {
    details: GetItemDetails,
    overview: ItemOverview,
}

impl Into<onep_api::Item> for GetItem {
    fn into(self) -> onep_api::Item {
        onep_api::Item {
            title: self.overview.title,
            fields: self
                .details
                .fields
                .into_iter()
                .map(|f| f.into())
                .filter(|f: &onep_api::ItemField| !f.value.is_empty())
                .collect(),
            sections: self
                .details
                .sections
                .into_iter()
                .map(|v| onep_api::ItemSection {
                    name: v.title,
                    fields: v
                        .fields
                        .into_iter()
                        .map(|f| f.into())
                        .filter(|f: &onep_api::ItemField| !f.value.is_empty())
                        .collect(),
                })
                .collect(),
        }
    }
}

#[derive(Debug, Deserialize)]
struct GetItemDetails {
    #[serde(default)]
    fields: Vec<GetItemDetailsField>,
    #[serde(default)]
    sections: Vec<GetItemSection>,
}

#[derive(Debug, Deserialize)]
struct GetItemDetailsField {
    name: String,
    #[serde(rename = "designation")]
    field_type: String,
    value: Value,
}

impl Into<onep_api::ItemField> for GetItemDetailsField {
    fn into(self) -> onep_api::ItemField {
        onep_api::ItemField {
            name: self.field_type,
            value: match self.value {
                Value::Null => String::new(),
                Value::String(v) => v,
                Value::Number(v) => format!("{}", v),
                Value::Bool(v) => if v { "true" } else { "false" }.to_string(),
                _ => panic!("unknown item field type for {}", self.name),
            },
        }
    }
}

#[derive(Debug, Deserialize)]
struct GetItemSection {
    title: String,
    #[serde(default)]
    fields: Vec<GetItemSectionField>,
}

#[derive(Debug, Deserialize)]
struct GetItemSectionField {
    #[serde(rename = "k")]
    kind: String,
    #[serde(rename = "n")]
    name: String,
    #[serde(rename = "t")]
    field_type: String,
    #[serde(rename = "v", default)]
    value: Value,
}

impl Into<onep_api::ItemField> for GetItemSectionField {
    fn into(self) -> onep_api::ItemField {
        onep_api::ItemField {
            name: self.field_type,
            value: match self.value {
                Value::Null => String::new(),
                Value::String(v) => v,
                Value::Number(v) => format!("{}", v),
                Value::Bool(v) => if v { "true" } else { "false" }.to_string(),
                _ => panic!("unknown item field type for {}", self.name),
            },
        }
    }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateItem {
    uuid: String,
    vault_uuid: String,
}

pub struct OnepasswordOp {}

fn exec<I, S>(args: I) -> Result<Vec<u8>, Error>
where
    I: IntoIterator<Item = S>,
    S: AsRef<std::ffi::OsStr>,
{
    let cmd = Command::new("op")
        .args(args)
        .output()
        .map_err(Error::Exec)?;

    if cmd.status.success() {
        Ok(cmd.stdout)
    } else {
        Err(Error::Backend(
            std::str::from_utf8(&cmd.stderr)?.to_string(),
        ))
    }
}

impl onep_api::OnePassword for OnepasswordOp {
    type Error = Error;

    fn totp(&self, uuid: &str) -> Result<String, Self::Error> {
        Ok(std::str::from_utf8(&exec(&["get", "totp", uuid])?)?.to_string())
    }

    fn account(&self) -> Result<onep_api::AccountMetadata, Self::Error> {
        let ret: GetAccount = serde_json::from_slice(&exec(&["get", "account"])?)?;

        Ok(ret.into())
    }

    fn vaults(&self) -> Result<Vec<onep_api::VaultMetadata>, Self::Error> {
        let ret: Vec<ListVault> = serde_json::from_slice(&exec(&["list", "vaults"])?)?;

        Ok(ret.into_iter().map(|v| v.into()).collect())
    }

    #[allow(clippy::filter_map)]
    fn search(&self, terms: Option<&str>) -> Result<Vec<onep_api::ItemMetadata>, Self::Error> {
        let ret: Vec<ListItem> = serde_json::from_slice(&exec(&["list", "items"])?)?;

        Ok(ret
            .into_iter()
            .filter(|v| {
                if let Some(terms) = terms {
                    v.uuid == terms
                        || v.vault_uuid == terms
                        || v.overview.urls.iter().any(|v| v.url.contains(terms))
                        || v.overview.title.contains(terms)
                        || v.overview.account_info.contains(terms)
                        || v.overview.tags.iter().any(|v| v.contains(terms))
                } else {
                    true
                }
            })
            .map(|v| v.into())
            .collect())
    }

    fn get(&self, uuid: &str) -> Result<Option<onep_api::Item>, Self::Error> {
        let ret: GetItem = serde_json::from_slice(&exec(&["get", "item", uuid])?)?;

        Ok(Some(ret.into()))
    }

    fn generate(
        &self,
        name: &str,
        username: Option<&str>,
        url: Option<&str>,
        tags: Option<&str>,
    ) -> Result<onep_api::Item, Self::Error> {
        let mut args = Vec::with_capacity(12);

        args.push(Cow::Borrowed("create"));
        args.push(Cow::Borrowed("item"));
        args.push(Cow::Borrowed("Login"));
        args.push(Cow::Borrowed("--generate-password"));
        args.push(Cow::Borrowed("--title"));
        args.push(Cow::Borrowed(name));

        if let Some(url) = url {
            args.push(Cow::Borrowed("--url"));
            args.push(Cow::Borrowed(url));
        }

        if let Some(tags) = tags {
            args.push(Cow::Borrowed("--tags"));
            args.push(Cow::Borrowed(tags));
        }

        if let Some(username) = username {
            args.push(Cow::Owned(format!("username={}", username)));
        }

        let ret: CreateItem = serde_json::from_slice(&exec(args.iter().map(Cow::as_ref))?)?;

        Ok(self.get(&ret.uuid)?.unwrap_or_else(|| unreachable!()))
    }
}
diff --git a/onep-api/Cargo.toml b/onep-api/Cargo.toml
deleted file mode 100644
index 09bee57..0000000
--- a/onep-api/Cargo.toml
+++ /dev/null
@@ -1,9 +0,0 @@
[package]
name = "onep-api"
version = "0.1.0"
authors = ["Jordan Doyle <jordan@doyle.la>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
\ No newline at end of file
diff --git a/onep-api/src/lib.rs b/onep-api/src/lib.rs
deleted file mode 100644
index 549fa50..0000000
--- a/onep-api/src/lib.rs
+++ /dev/null
@@ -1,58 +0,0 @@
#![deny(clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]

#[derive(Debug)]
pub struct AccountMetadata {
    pub name: String,
    pub domain: String,
}

#[derive(Debug)]
pub struct VaultMetadata {
    pub uuid: String,
    pub name: String,
}

#[derive(Debug)]
pub struct ItemMetadata {
    pub uuid: String,
    pub vault_uuid: String,
    pub title: String,
    pub account_info: String,
}

#[derive(Debug)]
pub struct Item {
    pub title: String,
    pub fields: Vec<ItemField>,
    pub sections: Vec<ItemSection>,
}

#[derive(Debug)]
pub struct ItemField {
    pub name: String,
    pub value: String,
}

#[derive(Debug)]
pub struct ItemSection {
    pub name: String,
    pub fields: Vec<ItemField>,
}

pub trait OnePassword {
    type Error;

    fn totp(&self, uuid: &str) -> Result<String, Self::Error>;
    fn account(&self) -> Result<AccountMetadata, Self::Error>;
    fn vaults(&self) -> Result<Vec<VaultMetadata>, Self::Error>;
    fn search(&self, terms: Option<&str>) -> Result<Vec<ItemMetadata>, Self::Error>;
    fn get(&self, uuid: &str) -> Result<Option<Item>, Self::Error>;
    fn generate(
        &self,
        name: &str,
        username: Option<&str>,
        url: Option<&str>,
        tags: Option<&str>,
    ) -> Result<Item, Self::Error>;
}
diff --git a/onep-backend-api/Cargo.toml b/onep-backend-api/Cargo.toml
new file mode 100644
index 0000000..bc6ab17
--- /dev/null
+++ b/onep-backend-api/Cargo.toml
@@ -0,0 +1,9 @@
[package]
name = "onep-backend-api"
version = "0.1.0"
authors = ["Jordan Doyle <jordan@doyle.la>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
\ No newline at end of file
diff --git a/onep-backend-api/src/lib.rs b/onep-backend-api/src/lib.rs
new file mode 100644
index 0000000..2c95300
--- /dev/null
+++ b/onep-backend-api/src/lib.rs
@@ -0,0 +1,58 @@
#![deny(clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]

#[derive(Debug)]
pub struct AccountMetadata {
    pub name: String,
    pub domain: String,
}

#[derive(Debug)]
pub struct VaultMetadata {
    pub uuid: String,
    pub name: String,
}

#[derive(Debug)]
pub struct ItemMetadata {
    pub uuid: String,
    pub vault_uuid: String,
    pub title: String,
    pub account_info: String,
}

#[derive(Debug)]
pub struct Item {
    pub title: String,
    pub fields: Vec<ItemField>,
    pub sections: Vec<ItemSection>,
}

#[derive(Debug)]
pub struct ItemField {
    pub name: String,
    pub value: String,
}

#[derive(Debug)]
pub struct ItemSection {
    pub name: String,
    pub fields: Vec<ItemField>,
}

pub trait Backend {
    type Error;

    fn totp(&self, uuid: &str) -> Result<String, Self::Error>;
    fn account(&self) -> Result<AccountMetadata, Self::Error>;
    fn vaults(&self) -> Result<Vec<VaultMetadata>, Self::Error>;
    fn search(&self, terms: Option<&str>) -> Result<Vec<ItemMetadata>, Self::Error>;
    fn get(&self, uuid: &str) -> Result<Option<Item>, Self::Error>;
    fn generate(
        &self,
        name: &str,
        username: Option<&str>,
        url: Option<&str>,
        tags: Option<&str>,
    ) -> Result<Item, Self::Error>;
}
diff --git a/onep-backend-op/Cargo.toml b/onep-backend-op/Cargo.toml
new file mode 100644
index 0000000..b47a10d
--- /dev/null
+++ b/onep-backend-op/Cargo.toml
@@ -0,0 +1,14 @@
[package]
name = "onep-backend-op"
version = "0.1.0"
authors = ["Jordan Doyle <jordan@doyle.la>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
onep-backend-api = { path = "../onep-backend-api" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_with = "1.5.0-alpha.1"
thiserror = "1.0"
\ No newline at end of file
diff --git a/onep-backend-op/src/lib.rs b/onep-backend-op/src/lib.rs
new file mode 100644
index 0000000..027998f
--- /dev/null
+++ b/onep-backend-op/src/lib.rs
@@ -0,0 +1,302 @@
#![deny(clippy::pedantic)]

use onep_backend_api as api;
use serde::Deserialize;
use serde_json::Value;
use std::borrow::Cow;
use std::process::Command;

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("op backend returned an error:\n{0}")]
    Backend(String),
    #[error("failed to exec backend:\n{0}")]
    Exec(std::io::Error),
    #[error("failed to parse json from op:\n{0}")]
    Json(#[from] serde_json::error::Error),
    #[error("failed to convert op response to utf-8:\n{0}")]
    Utf8(#[from] std::str::Utf8Error),
}

#[derive(Debug, Deserialize)]
struct GetAccount {
    name: String,
    domain: String,
}

impl Into<api::AccountMetadata> for GetAccount {
    fn into(self) -> api::AccountMetadata {
        api::AccountMetadata {
            name: self.name,
            domain: self.domain,
        }
    }
}

#[derive(Debug, Deserialize)]
struct ListVault {
    uuid: String,
    name: String,
}

impl Into<api::VaultMetadata> for ListVault {
    fn into(self) -> api::VaultMetadata {
        api::VaultMetadata {
            uuid: self.uuid,
            name: self.name,
        }
    }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ListItem {
    uuid: String,
    vault_uuid: String,
    created_at: String,
    updated_at: String,
    overview: ItemOverview,
}

impl Into<api::ItemMetadata> for ListItem {
    fn into(self) -> api::ItemMetadata {
        api::ItemMetadata {
            title: self.overview.title,
            account_info: self.overview.account_info,
            uuid: self.uuid,
            vault_uuid: self.vault_uuid,
        }
    }
}

#[derive(Debug, Deserialize)]
struct ItemOverview {
    #[serde(rename = "URLs", default)]
    urls: Vec<ItemOverviewUrl>,
    title: String,
    url: Option<String>,
    #[serde(rename = "ainfo")]
    account_info: String,
    #[serde(default)]
    tags: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct ItemOverviewUrl {
    #[serde(rename = "l")]
    label: String,
    #[serde(rename = "u")]
    url: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GetItem {
    details: GetItemDetails,
    overview: ItemOverview,
}

impl Into<api::Item> for GetItem {
    fn into(self) -> api::Item {
        api::Item {
            title: self.overview.title,
            fields: self
                .details
                .fields
                .into_iter()
                .map(|f| f.into())
                .filter(|f: &api::ItemField| !f.value.is_empty())
                .collect(),
            sections: self
                .details
                .sections
                .into_iter()
                .map(|v| api::ItemSection {
                    name: v.title,
                    fields: v
                        .fields
                        .into_iter()
                        .map(|f| f.into())
                        .filter(|f: &api::ItemField| !f.value.is_empty())
                        .collect(),
                })
                .collect(),
        }
    }
}

#[derive(Debug, Deserialize)]
struct GetItemDetails {
    #[serde(default)]
    fields: Vec<GetItemDetailsField>,
    #[serde(default)]
    sections: Vec<GetItemSection>,
}

#[derive(Debug, Deserialize)]
struct GetItemDetailsField {
    name: String,
    #[serde(rename = "designation")]
    field_type: String,
    value: Value,
}

impl Into<api::ItemField> for GetItemDetailsField {
    fn into(self) -> api::ItemField {
        api::ItemField {
            name: self.field_type,
            value: match self.value {
                Value::Null => String::new(),
                Value::String(v) => v,
                Value::Number(v) => format!("{}", v),
                Value::Bool(v) => if v { "true" } else { "false" }.to_string(),
                _ => panic!("unknown item field type for {}", self.name),
            },
        }
    }
}

#[derive(Debug, Deserialize)]
struct GetItemSection {
    title: String,
    #[serde(default)]
    fields: Vec<GetItemSectionField>,
}

#[derive(Debug, Deserialize)]
struct GetItemSectionField {
    #[serde(rename = "k")]
    kind: String,
    #[serde(rename = "n")]
    name: String,
    #[serde(rename = "t")]
    field_type: String,
    #[serde(rename = "v", default)]
    value: Value,
}

impl Into<api::ItemField> for GetItemSectionField {
    fn into(self) -> api::ItemField {
        api::ItemField {
            name: self.field_type,
            value: match self.value {
                Value::Null => String::new(),
                Value::String(v) => v,
                Value::Number(v) => format!("{}", v),
                Value::Bool(v) => if v { "true" } else { "false" }.to_string(),
                _ => panic!("unknown item field type for {}", self.name),
            },
        }
    }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateItem {
    uuid: String,
    vault_uuid: String,
}

pub struct OpBackend {}

fn exec<I, S>(args: I) -> Result<Vec<u8>, Error>
where
    I: IntoIterator<Item = S>,
    S: AsRef<std::ffi::OsStr>,
{
    let cmd = Command::new("op")
        .args(args)
        .output()
        .map_err(Error::Exec)?;

    if cmd.status.success() {
        Ok(cmd.stdout)
    } else {
        Err(Error::Backend(
            std::str::from_utf8(&cmd.stderr)?.to_string(),
        ))
    }
}

impl api::Backend for OpBackend {
    type Error = Error;

    fn totp(&self, uuid: &str) -> Result<String, Self::Error> {
        Ok(std::str::from_utf8(&exec(&["get", "totp", uuid])?)?.to_string())
    }

    fn account(&self) -> Result<api::AccountMetadata, Self::Error> {
        let ret: GetAccount = serde_json::from_slice(&exec(&["get", "account"])?)?;

        Ok(ret.into())
    }

    fn vaults(&self) -> Result<Vec<api::VaultMetadata>, Self::Error> {
        let ret: Vec<ListVault> = serde_json::from_slice(&exec(&["list", "vaults"])?)?;

        Ok(ret.into_iter().map(|v| v.into()).collect())
    }

    #[allow(clippy::filter_map)]
    fn search(&self, terms: Option<&str>) -> Result<Vec<api::ItemMetadata>, Self::Error> {
        let ret: Vec<ListItem> = serde_json::from_slice(&exec(&["list", "items"])?)?;

        Ok(ret
            .into_iter()
            .filter(|v| {
                if let Some(terms) = terms {
                    v.uuid == terms
                        || v.vault_uuid == terms
                        || v.overview.urls.iter().any(|v| v.url.contains(terms))
                        || v.overview.title.contains(terms)
                        || v.overview.account_info.contains(terms)
                        || v.overview.tags.iter().any(|v| v.contains(terms))
                } else {
                    true
                }
            })
            .map(|v| v.into())
            .collect())
    }

    fn get(&self, uuid: &str) -> Result<Option<api::Item>, Self::Error> {
        let ret: GetItem = serde_json::from_slice(&exec(&["get", "item", uuid])?)?;

        Ok(Some(ret.into()))
    }

    fn generate(
        &self,
        name: &str,
        username: Option<&str>,
        url: Option<&str>,
        tags: Option<&str>,
    ) -> Result<api::Item, Self::Error> {
        let mut args = Vec::with_capacity(12);

        args.push(Cow::Borrowed("create"));
        args.push(Cow::Borrowed("item"));
        args.push(Cow::Borrowed("Login"));
        args.push(Cow::Borrowed("--generate-password"));
        args.push(Cow::Borrowed("--title"));
        args.push(Cow::Borrowed(name));

        if let Some(url) = url {
            args.push(Cow::Borrowed("--url"));
            args.push(Cow::Borrowed(url));
        }

        if let Some(tags) = tags {
            args.push(Cow::Borrowed("--tags"));
            args.push(Cow::Borrowed(tags));
        }

        if let Some(username) = username {
            args.push(Cow::Owned(format!("username={}", username)));
        }

        let ret: CreateItem = serde_json::from_slice(&exec(args.iter().map(Cow::as_ref))?)?;

        Ok(self.get(&ret.uuid)?.unwrap_or_else(|| unreachable!()))
    }
}
diff --git a/onep-cli/Cargo.toml b/onep-cli/Cargo.toml
index 64745e2..9546bce 100644
--- a/onep-cli/Cargo.toml
+++ b/onep-cli/Cargo.toml
@@ -11,8 +11,8 @@ path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
onep-api = { path = "../onep-api" }
onep-api-op = { path = "../onep-api-op" }
onep-backend-api = { path = "../onep-backend-api" }
onep-backend-op = { path = "../onep-backend-op" }

clap = { git = "https://github.com/clap-rs/clap" }
term-table = "1.3"
diff --git a/onep-cli/src/main.rs b/onep-cli/src/main.rs
index a0e07ed..c809d75 100644
--- a/onep-cli/src/main.rs
+++ b/onep-cli/src/main.rs
@@ -3,7 +3,8 @@
use clap::Clap;
use colored::Colorize;
use itertools::Itertools;
use onep_api::OnePassword;
use onep_backend_api as api;
use onep_backend_op as backend;
use term_table::{
    row::Row,
    table_cell::{Alignment, TableCell},
@@ -53,14 +54,14 @@ enum Opt {
}

fn main() {
    if let Err(e) = run(&onep_api_op::OnepasswordOp {}) {
    if let Err(e) = run(&backend::OpBackend {}) {
        eprintln!("{}", e);
        std::process::exit(1);
    }
}

#[allow(clippy::non_ascii_literal)]
fn run<T: OnePassword>(imp: &T) -> anyhow::Result<()>
fn run<T: api::Backend>(imp: &T) -> anyhow::Result<()>
where
    T::Error: 'static + std::error::Error + Send + Sync,
{
@@ -167,7 +168,7 @@ where
    Ok(())
}

fn show(item: onep_api::Item) {
fn show(item: api::Item) {
    let mut table = Table::new();
    table.style = TableStyle::extended();