Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update 1password to the version 2 CLI #11692

Merged
merged 1 commit into from
Feb 9, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 57 additions & 72 deletions crates/credential/cargo-credential-1password/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,30 @@ const CARGO_TAG: &str = "cargo-registry";
struct OnePasswordKeychain {
account: 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)]
struct Login {
details: Details,
}

#[derive(Deserialize)]
struct Details {
fields: Vec<Field>,
}

#[derive(Deserialize)]
struct Field {
designation: String,
value: String,
id: String,
value: Option<String>,
}

/// 1password item from `op list items`.
/// 1password item from `op items list`.
#[derive(Deserialize)]
struct ListItem {
uuid: String,
overview: Overview,
id: String,
urls: Vec<Url>,
}

#[derive(Deserialize)]
struct Overview {
url: String,
struct Url {
href: String,
}

impl OnePasswordKeychain {
Expand All @@ -50,8 +43,6 @@ impl OnePasswordKeychain {
let mut action = false;
let mut account = None;
let mut vault = None;
let mut sign_in_address = None;
let mut email = None;
while let Some(arg) = args.next() {
match arg.as_str() {
"--account" => {
Expand All @@ -60,12 +51,6 @@ impl OnePasswordKeychain {
"--vault" => {
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('-') => {
return Err(format!("unknown option {}", s).into());
}
Expand All @@ -78,15 +63,7 @@ impl OnePasswordKeychain {
}
}
}
if sign_in_address.is_none() && email.is_some() {
return Err("--email requires --sign-in-address".into());
}
Ok(OnePasswordKeychain {
account,
vault,
sign_in_address,
email,
})
Ok(OnePasswordKeychain { account, vault })
}

fn signin(&self) -> Result<Option<String>, Error> {
Expand All @@ -96,24 +73,9 @@ impl OnePasswordKeychain {
return Ok(None);
}
let mut cmd = Command::new("op");
cmd.arg("signin");
if let Some(addr) = &self.sign_in_address {
cmd.arg(addr);
if let Some(email) = &self.email {
cmd.arg(email);
}
}
cmd.arg("--raw");
cmd.args(&["signin", "--raw"]);
cmd.stdout(Stdio::piped());
#[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);
self.with_tty(&mut cmd)?;
let mut child = cmd
.spawn()
.map_err(|e| format!("failed to spawn `op`: {}", e))?;
Expand All @@ -133,6 +95,11 @@ impl OnePasswordKeychain {
if !status.success() {
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))
}

Expand All @@ -154,6 +121,19 @@ impl OnePasswordKeychain {
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> {
cmd.stdout(Stdio::piped());
let mut child = cmd
Expand All @@ -179,20 +159,22 @@ impl OnePasswordKeychain {
let cmd = self.make_cmd(
session,
&[
"list",
"items",
"list",
"--categories",
"Login",
"--tags",
CARGO_TAG,
"--format",
"json",
],
);
let buffer = self.run_cmd(cmd)?;
let items: Vec<ListItem> = serde_json::from_str(&buffer)
.map_err(|e| format!("failed to deserialize JSON from 1password list: {}", e))?;
let mut matches = items
.into_iter()
.filter(|item| item.overview.url == index_url);
.filter(|item| item.urls.iter().any(|url| url.href == index_url));
match matches.next() {
Some(login) => {
// Should this maybe just sort on `updatedAt` and return the newest one?
Expand All @@ -204,7 +186,7 @@ impl OnePasswordKeychain {
)
.into());
}
Ok(Some(login.uuid))
Ok(Some(login.id))
}
None => Ok(None),
}
Expand All @@ -213,13 +195,13 @@ impl OnePasswordKeychain {
fn modify(
&self,
session: &Option<String>,
uuid: &str,
id: &str,
token: &str,
_name: Option<&str>,
) -> Result<(), Error> {
let cmd = self.make_cmd(
session,
&["edit", "item", uuid, &format!("password={}", token)],
&["item", "edit", id, &format!("password={}", token)],
);
self.run_cmd(cmd)?;
Ok(())
Expand All @@ -236,11 +218,12 @@ impl OnePasswordKeychain {
Some(name) => format!("Cargo registry token for {}", name),
None => "Cargo registry token".to_string(),
};
let cmd = self.make_cmd(
let mut cmd = self.make_cmd(
session,
&[
"create",
"item",
"create",
"--category",
"Login",
&format!("password={}", token),
&format!("url={}", index_url),
Expand All @@ -250,28 +233,30 @@ impl OnePasswordKeychain {
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)?;
Ok(())
}

fn get_token(&self, session: &Option<String>, uuid: &str) -> Result<String, Error> {
let cmd = self.make_cmd(session, &["get", "item", uuid]);
fn get_token(&self, session: &Option<String>, id: &str) -> Result<String, Error> {
let cmd = self.make_cmd(session, &["item", "get", "--format=json", id]);
let buffer = self.run_cmd(cmd)?;
let item: Login = serde_json::from_str(&buffer)
.map_err(|e| format!("failed to deserialize JSON from 1password get: {}", e))?;
let password = item
.details
.fields
.into_iter()
.find(|item| item.designation == "password");
let password = item.fields.into_iter().find(|item| item.id == "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()),
}
}

fn delete(&self, session: &Option<String>, uuid: &str) -> Result<(), Error> {
let cmd = self.make_cmd(session, &["delete", "item", uuid]);
fn delete(&self, session: &Option<String>, id: &str) -> Result<(), Error> {
let cmd = self.make_cmd(session, &["item", "delete", id]);
self.run_cmd(cmd)?;
Ok(())
}
Expand All @@ -284,8 +269,8 @@ impl Credential for OnePasswordKeychain {

fn get(&self, index_url: &str) -> Result<String, Error> {
let session = self.signin()?;
if let Some(uuid) = self.search(&session, index_url)? {
self.get_token(&session, &uuid)
if let Some(id) = self.search(&session, index_url)? {
self.get_token(&session, &id)
} else {
return Err(format!(
"no 1password entry found for registry `{}`, try `cargo login` to add a token",
Expand All @@ -298,8 +283,8 @@ impl Credential for OnePasswordKeychain {
fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
let session = self.signin()?;
// Check if an item already exists.
if let Some(uuid) = self.search(&session, index_url)? {
self.modify(&session, &uuid, token, name)
if let Some(id) = self.search(&session, index_url)? {
self.modify(&session, &id, token, name)
} else {
self.create(&session, index_url, token, name)
}
Expand All @@ -308,8 +293,8 @@ impl Credential for OnePasswordKeychain {
fn erase(&self, index_url: &str) -> Result<(), Error> {
let session = self.signin()?;
// Check if an item already exists.
if let Some(uuid) = self.search(&session, index_url)? {
self.delete(&session, &uuid)?;
if let Some(id) = self.search(&session, index_url)? {
self.delete(&session, &id)?;
} else {
eprintln!("not currently logged in to `{}`", index_url);
}
Expand Down