Update 1password to the version 2 CLI

This commit is contained in:
Eric Huss 2023-02-08 21:08:17 -08:00
parent 82c3bb79e3
commit 7a67332d3c

View File

@ -11,37 +11,30 @@ const CARGO_TAG: &str = "cargo-registry";
struct OnePasswordKeychain { struct OnePasswordKeychain {
account: Option<String>, account: Option<String>,
vault: Option<String>, vault: Option<String>,
sign_in_address: Option<String>,
email: Option<String>,
} }
/// 1password Login item type, used for the JSON output of `op get item`. /// 1password Login item type, used for the JSON output of `op item get`.
#[derive(Deserialize)] #[derive(Deserialize)]
struct Login { struct Login {
details: Details,
}
#[derive(Deserialize)]
struct Details {
fields: Vec<Field>, fields: Vec<Field>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct Field { struct Field {
designation: String, id: String,
value: String, value: Option<String>,
} }
/// 1password item from `op list items`. /// 1password item from `op items list`.
#[derive(Deserialize)] #[derive(Deserialize)]
struct ListItem { struct ListItem {
uuid: String, id: String,
overview: Overview, urls: Vec<Url>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct Overview { struct Url {
url: String, href: String,
} }
impl OnePasswordKeychain { impl OnePasswordKeychain {
@ -50,8 +43,6 @@ impl OnePasswordKeychain {
let mut action = false; let mut action = false;
let mut account = None; let mut account = None;
let mut vault = None; let mut vault = None;
let mut sign_in_address = None;
let mut email = None;
while let Some(arg) = args.next() { while let Some(arg) = args.next() {
match arg.as_str() { match arg.as_str() {
"--account" => { "--account" => {
@ -60,12 +51,6 @@ impl OnePasswordKeychain {
"--vault" => { "--vault" => {
vault = Some(args.next().ok_or("--vault needs an arg")?); vault = Some(args.next().ok_or("--vault needs an arg")?);
} }
"--sign-in-address" => {
sign_in_address = Some(args.next().ok_or("--sign-in-address needs an arg")?);
}
"--email" => {
email = Some(args.next().ok_or("--email needs an arg")?);
}
s if s.starts_with('-') => { s if s.starts_with('-') => {
return Err(format!("unknown option {}", s).into()); return Err(format!("unknown option {}", s).into());
} }
@ -78,15 +63,7 @@ impl OnePasswordKeychain {
} }
} }
} }
if sign_in_address.is_none() && email.is_some() { Ok(OnePasswordKeychain { account, vault })
return Err("--email requires --sign-in-address".into());
}
Ok(OnePasswordKeychain {
account,
vault,
sign_in_address,
email,
})
} }
fn signin(&self) -> Result<Option<String>, Error> { fn signin(&self) -> Result<Option<String>, Error> {
@ -96,24 +73,9 @@ impl OnePasswordKeychain {
return Ok(None); return Ok(None);
} }
let mut cmd = Command::new("op"); let mut cmd = Command::new("op");
cmd.arg("signin"); cmd.args(&["signin", "--raw"]);
if let Some(addr) = &self.sign_in_address {
cmd.arg(addr);
if let Some(email) = &self.email {
cmd.arg(email);
}
}
cmd.arg("--raw");
cmd.stdout(Stdio::piped()); cmd.stdout(Stdio::piped());
#[cfg(unix)] self.with_tty(&mut cmd)?;
const IN_DEVICE: &str = "/dev/tty";
#[cfg(windows)]
const IN_DEVICE: &str = "CONIN$";
let stdin = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(IN_DEVICE)?;
cmd.stdin(stdin);
let mut child = cmd let mut child = cmd
.spawn() .spawn()
.map_err(|e| format!("failed to spawn `op`: {}", e))?; .map_err(|e| format!("failed to spawn `op`: {}", e))?;
@ -133,6 +95,11 @@ impl OnePasswordKeychain {
if !status.success() { if !status.success() {
return Err(format!("failed to run `op signin`: {}", status).into()); return Err(format!("failed to run `op signin`: {}", status).into());
} }
if buffer.is_empty() {
// When using CLI integration, `op signin` returns no output,
// so there is no need to set the session.
return Ok(None);
}
Ok(Some(buffer)) Ok(Some(buffer))
} }
@ -154,6 +121,19 @@ impl OnePasswordKeychain {
cmd cmd
} }
fn with_tty(&self, cmd: &mut Command) -> Result<(), Error> {
#[cfg(unix)]
const IN_DEVICE: &str = "/dev/tty";
#[cfg(windows)]
const IN_DEVICE: &str = "CONIN$";
let stdin = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(IN_DEVICE)?;
cmd.stdin(stdin);
Ok(())
}
fn run_cmd(&self, mut cmd: Command) -> Result<String, Error> { fn run_cmd(&self, mut cmd: Command) -> Result<String, Error> {
cmd.stdout(Stdio::piped()); cmd.stdout(Stdio::piped());
let mut child = cmd let mut child = cmd
@ -179,12 +159,14 @@ impl OnePasswordKeychain {
let cmd = self.make_cmd( let cmd = self.make_cmd(
session, session,
&[ &[
"list",
"items", "items",
"list",
"--categories", "--categories",
"Login", "Login",
"--tags", "--tags",
CARGO_TAG, CARGO_TAG,
"--format",
"json",
], ],
); );
let buffer = self.run_cmd(cmd)?; let buffer = self.run_cmd(cmd)?;
@ -192,7 +174,7 @@ impl OnePasswordKeychain {
.map_err(|e| format!("failed to deserialize JSON from 1password list: {}", e))?; .map_err(|e| format!("failed to deserialize JSON from 1password list: {}", e))?;
let mut matches = items let mut matches = items
.into_iter() .into_iter()
.filter(|item| item.overview.url == index_url); .filter(|item| item.urls.iter().any(|url| url.href == index_url));
match matches.next() { match matches.next() {
Some(login) => { Some(login) => {
// Should this maybe just sort on `updatedAt` and return the newest one? // Should this maybe just sort on `updatedAt` and return the newest one?
@ -204,7 +186,7 @@ impl OnePasswordKeychain {
) )
.into()); .into());
} }
Ok(Some(login.uuid)) Ok(Some(login.id))
} }
None => Ok(None), None => Ok(None),
} }
@ -213,13 +195,13 @@ impl OnePasswordKeychain {
fn modify( fn modify(
&self, &self,
session: &Option<String>, session: &Option<String>,
uuid: &str, id: &str,
token: &str, token: &str,
_name: Option<&str>, _name: Option<&str>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let cmd = self.make_cmd( let cmd = self.make_cmd(
session, session,
&["edit", "item", uuid, &format!("password={}", token)], &["item", "edit", id, &format!("password={}", token)],
); );
self.run_cmd(cmd)?; self.run_cmd(cmd)?;
Ok(()) Ok(())
@ -236,11 +218,12 @@ impl OnePasswordKeychain {
Some(name) => format!("Cargo registry token for {}", name), Some(name) => format!("Cargo registry token for {}", name),
None => "Cargo registry token".to_string(), None => "Cargo registry token".to_string(),
}; };
let cmd = self.make_cmd( let mut cmd = self.make_cmd(
session, session,
&[ &[
"create",
"item", "item",
"create",
"--category",
"Login", "Login",
&format!("password={}", token), &format!("password={}", token),
&format!("url={}", index_url), &format!("url={}", index_url),
@ -250,28 +233,30 @@ impl OnePasswordKeychain {
CARGO_TAG, CARGO_TAG,
], ],
); );
// For unknown reasons, `op item create` seems to not be happy if
// stdin is not a tty. Otherwise it returns with a 0 exit code without
// doing anything.
self.with_tty(&mut cmd)?;
self.run_cmd(cmd)?; self.run_cmd(cmd)?;
Ok(()) Ok(())
} }
fn get_token(&self, session: &Option<String>, uuid: &str) -> Result<String, Error> { fn get_token(&self, session: &Option<String>, id: &str) -> Result<String, Error> {
let cmd = self.make_cmd(session, &["get", "item", uuid]); let cmd = self.make_cmd(session, &["item", "get", "--format=json", id]);
let buffer = self.run_cmd(cmd)?; let buffer = self.run_cmd(cmd)?;
let item: Login = serde_json::from_str(&buffer) let item: Login = serde_json::from_str(&buffer)
.map_err(|e| format!("failed to deserialize JSON from 1password get: {}", e))?; .map_err(|e| format!("failed to deserialize JSON from 1password get: {}", e))?;
let password = item let password = item.fields.into_iter().find(|item| item.id == "password");
.details
.fields
.into_iter()
.find(|item| item.designation == "password");
match password { match password {
Some(password) => Ok(password.value), Some(password) => password
.value
.ok_or_else(|| format!("missing password value for entry").into()),
None => Err("could not find password field".into()), None => Err("could not find password field".into()),
} }
} }
fn delete(&self, session: &Option<String>, uuid: &str) -> Result<(), Error> { fn delete(&self, session: &Option<String>, id: &str) -> Result<(), Error> {
let cmd = self.make_cmd(session, &["delete", "item", uuid]); let cmd = self.make_cmd(session, &["item", "delete", id]);
self.run_cmd(cmd)?; self.run_cmd(cmd)?;
Ok(()) Ok(())
} }
@ -284,8 +269,8 @@ impl Credential for OnePasswordKeychain {
fn get(&self, index_url: &str) -> Result<String, Error> { fn get(&self, index_url: &str) -> Result<String, Error> {
let session = self.signin()?; let session = self.signin()?;
if let Some(uuid) = self.search(&session, index_url)? { if let Some(id) = self.search(&session, index_url)? {
self.get_token(&session, &uuid) self.get_token(&session, &id)
} else { } else {
return Err(format!( return Err(format!(
"no 1password entry found for registry `{}`, try `cargo login` to add a token", "no 1password entry found for registry `{}`, try `cargo login` to add a token",
@ -298,8 +283,8 @@ impl Credential for OnePasswordKeychain {
fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> { fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
let session = self.signin()?; let session = self.signin()?;
// Check if an item already exists. // Check if an item already exists.
if let Some(uuid) = self.search(&session, index_url)? { if let Some(id) = self.search(&session, index_url)? {
self.modify(&session, &uuid, token, name) self.modify(&session, &id, token, name)
} else { } else {
self.create(&session, index_url, token, name) self.create(&session, index_url, token, name)
} }
@ -308,8 +293,8 @@ impl Credential for OnePasswordKeychain {
fn erase(&self, index_url: &str) -> Result<(), Error> { fn erase(&self, index_url: &str) -> Result<(), Error> {
let session = self.signin()?; let session = self.signin()?;
// Check if an item already exists. // Check if an item already exists.
if let Some(uuid) = self.search(&session, index_url)? { if let Some(id) = self.search(&session, index_url)? {
self.delete(&session, &uuid)?; self.delete(&session, &id)?;
} else { } else {
eprintln!("not currently logged in to `{}`", index_url); eprintln!("not currently logged in to `{}`", index_url);
} }