Skip to content

Commit e54a0b5

Browse files
committed
Auto merge of #11692 - ehuss:update-1password, r=epage
Update 1password to the version 2 CLI This updates the 1password credential manager integration to the version 2 `op` CLI. The new CLI changed the commands and output and behavior in various ways, documented at https://developer.1password.com/docs/cli/upgrade/. If you have 1password, you can test this by building the `cargo-credential-1password` binary. I recommend enabling CLI integration in the app, but using the manual `op signin` process also works. Once signed in, you can test it with: ```sh echo "this-is-my-token" | CARGO_REGISTRY_NAME_OPT=crates-io \ CARGO_REGISTRY_INDEX_URL=https://github.com/rust-lang/crates.io-index ./target/debug/cargo-credential-1password store CARGO_REGISTRY_INDEX_URL=https://github.com/rust-lang/crates.io-index ./target/debug/cargo-credential-1password get CARGO_REGISTRY_INDEX_URL=https://github.com/rust-lang/crates.io-index ./target/debug/cargo-credential-1password erase ```
2 parents 2286d5f + 7a67332 commit e54a0b5

File tree

1 file changed

+57
-72
lines changed
  • crates/credential/cargo-credential-1password/src

1 file changed

+57
-72
lines changed

crates/credential/cargo-credential-1password/src/main.rs

Lines changed: 57 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,30 @@ const CARGO_TAG: &str = "cargo-registry";
1111
struct OnePasswordKeychain {
1212
account: Option<String>,
1313
vault: Option<String>,
14-
sign_in_address: Option<String>,
15-
email: Option<String>,
1614
}
1715

18-
/// 1password Login item type, used for the JSON output of `op get item`.
16+
/// 1password Login item type, used for the JSON output of `op item get`.
1917
#[derive(Deserialize)]
2018
struct Login {
21-
details: Details,
22-
}
23-
24-
#[derive(Deserialize)]
25-
struct Details {
2619
fields: Vec<Field>,
2720
}
2821

2922
#[derive(Deserialize)]
3023
struct Field {
31-
designation: String,
32-
value: String,
24+
id: String,
25+
value: Option<String>,
3326
}
3427

35-
/// 1password item from `op list items`.
28+
/// 1password item from `op items list`.
3629
#[derive(Deserialize)]
3730
struct ListItem {
38-
uuid: String,
39-
overview: Overview,
31+
id: String,
32+
urls: Vec<Url>,
4033
}
4134

4235
#[derive(Deserialize)]
43-
struct Overview {
44-
url: String,
36+
struct Url {
37+
href: String,
4538
}
4639

4740
impl OnePasswordKeychain {
@@ -50,8 +43,6 @@ impl OnePasswordKeychain {
5043
let mut action = false;
5144
let mut account = None;
5245
let mut vault = None;
53-
let mut sign_in_address = None;
54-
let mut email = None;
5546
while let Some(arg) = args.next() {
5647
match arg.as_str() {
5748
"--account" => {
@@ -60,12 +51,6 @@ impl OnePasswordKeychain {
6051
"--vault" => {
6152
vault = Some(args.next().ok_or("--vault needs an arg")?);
6253
}
63-
"--sign-in-address" => {
64-
sign_in_address = Some(args.next().ok_or("--sign-in-address needs an arg")?);
65-
}
66-
"--email" => {
67-
email = Some(args.next().ok_or("--email needs an arg")?);
68-
}
6954
s if s.starts_with('-') => {
7055
return Err(format!("unknown option {}", s).into());
7156
}
@@ -78,15 +63,7 @@ impl OnePasswordKeychain {
7863
}
7964
}
8065
}
81-
if sign_in_address.is_none() && email.is_some() {
82-
return Err("--email requires --sign-in-address".into());
83-
}
84-
Ok(OnePasswordKeychain {
85-
account,
86-
vault,
87-
sign_in_address,
88-
email,
89-
})
66+
Ok(OnePasswordKeychain { account, vault })
9067
}
9168

9269
fn signin(&self) -> Result<Option<String>, Error> {
@@ -96,24 +73,9 @@ impl OnePasswordKeychain {
9673
return Ok(None);
9774
}
9875
let mut cmd = Command::new("op");
99-
cmd.arg("signin");
100-
if let Some(addr) = &self.sign_in_address {
101-
cmd.arg(addr);
102-
if let Some(email) = &self.email {
103-
cmd.arg(email);
104-
}
105-
}
106-
cmd.arg("--raw");
76+
cmd.args(&["signin", "--raw"]);
10777
cmd.stdout(Stdio::piped());
108-
#[cfg(unix)]
109-
const IN_DEVICE: &str = "/dev/tty";
110-
#[cfg(windows)]
111-
const IN_DEVICE: &str = "CONIN$";
112-
let stdin = std::fs::OpenOptions::new()
113-
.read(true)
114-
.write(true)
115-
.open(IN_DEVICE)?;
116-
cmd.stdin(stdin);
78+
self.with_tty(&mut cmd)?;
11779
let mut child = cmd
11880
.spawn()
11981
.map_err(|e| format!("failed to spawn `op`: {}", e))?;
@@ -133,6 +95,11 @@ impl OnePasswordKeychain {
13395
if !status.success() {
13496
return Err(format!("failed to run `op signin`: {}", status).into());
13597
}
98+
if buffer.is_empty() {
99+
// When using CLI integration, `op signin` returns no output,
100+
// so there is no need to set the session.
101+
return Ok(None);
102+
}
136103
Ok(Some(buffer))
137104
}
138105

@@ -154,6 +121,19 @@ impl OnePasswordKeychain {
154121
cmd
155122
}
156123

124+
fn with_tty(&self, cmd: &mut Command) -> Result<(), Error> {
125+
#[cfg(unix)]
126+
const IN_DEVICE: &str = "/dev/tty";
127+
#[cfg(windows)]
128+
const IN_DEVICE: &str = "CONIN$";
129+
let stdin = std::fs::OpenOptions::new()
130+
.read(true)
131+
.write(true)
132+
.open(IN_DEVICE)?;
133+
cmd.stdin(stdin);
134+
Ok(())
135+
}
136+
157137
fn run_cmd(&self, mut cmd: Command) -> Result<String, Error> {
158138
cmd.stdout(Stdio::piped());
159139
let mut child = cmd
@@ -179,20 +159,22 @@ impl OnePasswordKeychain {
179159
let cmd = self.make_cmd(
180160
session,
181161
&[
182-
"list",
183162
"items",
163+
"list",
184164
"--categories",
185165
"Login",
186166
"--tags",
187167
CARGO_TAG,
168+
"--format",
169+
"json",
188170
],
189171
);
190172
let buffer = self.run_cmd(cmd)?;
191173
let items: Vec<ListItem> = serde_json::from_str(&buffer)
192174
.map_err(|e| format!("failed to deserialize JSON from 1password list: {}", e))?;
193175
let mut matches = items
194176
.into_iter()
195-
.filter(|item| item.overview.url == index_url);
177+
.filter(|item| item.urls.iter().any(|url| url.href == index_url));
196178
match matches.next() {
197179
Some(login) => {
198180
// Should this maybe just sort on `updatedAt` and return the newest one?
@@ -204,7 +186,7 @@ impl OnePasswordKeychain {
204186
)
205187
.into());
206188
}
207-
Ok(Some(login.uuid))
189+
Ok(Some(login.id))
208190
}
209191
None => Ok(None),
210192
}
@@ -213,13 +195,13 @@ impl OnePasswordKeychain {
213195
fn modify(
214196
&self,
215197
session: &Option<String>,
216-
uuid: &str,
198+
id: &str,
217199
token: &str,
218200
_name: Option<&str>,
219201
) -> Result<(), Error> {
220202
let cmd = self.make_cmd(
221203
session,
222-
&["edit", "item", uuid, &format!("password={}", token)],
204+
&["item", "edit", id, &format!("password={}", token)],
223205
);
224206
self.run_cmd(cmd)?;
225207
Ok(())
@@ -236,11 +218,12 @@ impl OnePasswordKeychain {
236218
Some(name) => format!("Cargo registry token for {}", name),
237219
None => "Cargo registry token".to_string(),
238220
};
239-
let cmd = self.make_cmd(
221+
let mut cmd = self.make_cmd(
240222
session,
241223
&[
242-
"create",
243224
"item",
225+
"create",
226+
"--category",
244227
"Login",
245228
&format!("password={}", token),
246229
&format!("url={}", index_url),
@@ -250,28 +233,30 @@ impl OnePasswordKeychain {
250233
CARGO_TAG,
251234
],
252235
);
236+
// For unknown reasons, `op item create` seems to not be happy if
237+
// stdin is not a tty. Otherwise it returns with a 0 exit code without
238+
// doing anything.
239+
self.with_tty(&mut cmd)?;
253240
self.run_cmd(cmd)?;
254241
Ok(())
255242
}
256243

257-
fn get_token(&self, session: &Option<String>, uuid: &str) -> Result<String, Error> {
258-
let cmd = self.make_cmd(session, &["get", "item", uuid]);
244+
fn get_token(&self, session: &Option<String>, id: &str) -> Result<String, Error> {
245+
let cmd = self.make_cmd(session, &["item", "get", "--format=json", id]);
259246
let buffer = self.run_cmd(cmd)?;
260247
let item: Login = serde_json::from_str(&buffer)
261248
.map_err(|e| format!("failed to deserialize JSON from 1password get: {}", e))?;
262-
let password = item
263-
.details
264-
.fields
265-
.into_iter()
266-
.find(|item| item.designation == "password");
249+
let password = item.fields.into_iter().find(|item| item.id == "password");
267250
match password {
268-
Some(password) => Ok(password.value),
251+
Some(password) => password
252+
.value
253+
.ok_or_else(|| format!("missing password value for entry").into()),
269254
None => Err("could not find password field".into()),
270255
}
271256
}
272257

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

285270
fn get(&self, index_url: &str) -> Result<String, Error> {
286271
let session = self.signin()?;
287-
if let Some(uuid) = self.search(&session, index_url)? {
288-
self.get_token(&session, &uuid)
272+
if let Some(id) = self.search(&session, index_url)? {
273+
self.get_token(&session, &id)
289274
} else {
290275
return Err(format!(
291276
"no 1password entry found for registry `{}`, try `cargo login` to add a token",
@@ -298,8 +283,8 @@ impl Credential for OnePasswordKeychain {
298283
fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
299284
let session = self.signin()?;
300285
// Check if an item already exists.
301-
if let Some(uuid) = self.search(&session, index_url)? {
302-
self.modify(&session, &uuid, token, name)
286+
if let Some(id) = self.search(&session, index_url)? {
287+
self.modify(&session, &id, token, name)
303288
} else {
304289
self.create(&session, index_url, token, name)
305290
}
@@ -308,8 +293,8 @@ impl Credential for OnePasswordKeychain {
308293
fn erase(&self, index_url: &str) -> Result<(), Error> {
309294
let session = self.signin()?;
310295
// Check if an item already exists.
311-
if let Some(uuid) = self.search(&session, index_url)? {
312-
self.delete(&session, &uuid)?;
296+
if let Some(id) = self.search(&session, index_url)? {
297+
self.delete(&session, &id)?;
313298
} else {
314299
eprintln!("not currently logged in to `{}`", index_url);
315300
}

0 commit comments

Comments
 (0)