PageRenderTime 49ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

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

https://gitlab.com/frewsxcv/cargo
Rust | 323 lines | 292 code | 22 blank | 9 comment | 25 complexity | dc8df1fa9dbaf053af53b9ced2d0afa3 MD5 | raw file
  1. //! Cargo registry 1password credential process.
  2. use cargo_credential::{Credential, Error};
  3. use serde::Deserialize;
  4. use std::io::Read;
  5. use std::process::{Command, Stdio};
  6. const CARGO_TAG: &str = "cargo-registry";
  7. /// Implementation of 1password keychain access for Cargo registries.
  8. struct OnePasswordKeychain {
  9. account: Option<String>,
  10. vault: Option<String>,
  11. sign_in_address: Option<String>,
  12. email: Option<String>,
  13. }
  14. /// 1password Login item type, used for the JSON output of `op get item`.
  15. #[derive(Deserialize)]
  16. struct Login {
  17. details: Details,
  18. }
  19. #[derive(Deserialize)]
  20. struct Details {
  21. fields: Vec<Field>,
  22. }
  23. #[derive(Deserialize)]
  24. struct Field {
  25. designation: String,
  26. value: String,
  27. }
  28. /// 1password item from `op list items`.
  29. #[derive(Deserialize)]
  30. struct ListItem {
  31. uuid: String,
  32. overview: Overview,
  33. }
  34. #[derive(Deserialize)]
  35. struct Overview {
  36. title: String,
  37. }
  38. impl OnePasswordKeychain {
  39. fn new() -> Result<OnePasswordKeychain, Error> {
  40. let mut args = std::env::args().skip(1);
  41. let mut action = false;
  42. let mut account = None;
  43. let mut vault = None;
  44. let mut sign_in_address = None;
  45. let mut email = None;
  46. while let Some(arg) = args.next() {
  47. match arg.as_str() {
  48. "--account" => {
  49. account = Some(args.next().ok_or("--account needs an arg")?);
  50. }
  51. "--vault" => {
  52. vault = Some(args.next().ok_or("--vault needs an arg")?);
  53. }
  54. "--sign-in-address" => {
  55. sign_in_address = Some(args.next().ok_or("--sign-in-address needs an arg")?);
  56. }
  57. "--email" => {
  58. email = Some(args.next().ok_or("--email needs an arg")?);
  59. }
  60. s if s.starts_with('-') => {
  61. return Err(format!("unknown option {}", s).into());
  62. }
  63. _ => {
  64. if action {
  65. return Err("too many arguments".into());
  66. } else {
  67. action = true;
  68. }
  69. }
  70. }
  71. }
  72. if sign_in_address.is_none() && email.is_some() {
  73. return Err("--email requires --sign-in-address".into());
  74. }
  75. Ok(OnePasswordKeychain {
  76. account,
  77. vault,
  78. sign_in_address,
  79. email,
  80. })
  81. }
  82. fn signin(&self) -> Result<Option<String>, Error> {
  83. // If there are any session env vars, we'll assume that this is the
  84. // correct account, and that the user knows what they are doing.
  85. if std::env::vars().any(|(name, _)| name.starts_with("OP_SESSION_")) {
  86. return Ok(None);
  87. }
  88. let mut cmd = Command::new("op");
  89. cmd.arg("signin");
  90. if let Some(addr) = &self.sign_in_address {
  91. cmd.arg(addr);
  92. if let Some(email) = &self.email {
  93. cmd.arg(email);
  94. }
  95. }
  96. cmd.arg("--raw");
  97. cmd.stdout(Stdio::piped());
  98. #[cfg(unix)]
  99. const IN_DEVICE: &str = "/dev/tty";
  100. #[cfg(windows)]
  101. const IN_DEVICE: &str = "CONIN$";
  102. let stdin = std::fs::OpenOptions::new()
  103. .read(true)
  104. .write(true)
  105. .open(IN_DEVICE)?;
  106. cmd.stdin(stdin);
  107. let mut child = cmd
  108. .spawn()
  109. .map_err(|e| format!("failed to spawn `op`: {}", e))?;
  110. let mut buffer = String::new();
  111. child
  112. .stdout
  113. .as_mut()
  114. .unwrap()
  115. .read_to_string(&mut buffer)
  116. .map_err(|e| format!("failed to get session from `op`: {}", e))?;
  117. if let Some(end) = buffer.find('\n') {
  118. buffer.truncate(end);
  119. }
  120. let status = child
  121. .wait()
  122. .map_err(|e| format!("failed to wait for `op`: {}", e))?;
  123. if !status.success() {
  124. return Err(format!("failed to run `op signin`: {}", status).into());
  125. }
  126. Ok(Some(buffer))
  127. }
  128. fn make_cmd(&self, session: &Option<String>, args: &[&str]) -> Command {
  129. let mut cmd = Command::new("op");
  130. cmd.args(args);
  131. if let Some(account) = &self.account {
  132. cmd.arg("--account");
  133. cmd.arg(account);
  134. }
  135. if let Some(vault) = &self.vault {
  136. cmd.arg("--vault");
  137. cmd.arg(vault);
  138. }
  139. if let Some(session) = session {
  140. cmd.arg("--session");
  141. cmd.arg(session);
  142. }
  143. cmd
  144. }
  145. fn run_cmd(&self, mut cmd: Command) -> Result<String, Error> {
  146. cmd.stdout(Stdio::piped());
  147. let mut child = cmd
  148. .spawn()
  149. .map_err(|e| format!("failed to spawn `op`: {}", e))?;
  150. let mut buffer = String::new();
  151. child
  152. .stdout
  153. .as_mut()
  154. .unwrap()
  155. .read_to_string(&mut buffer)
  156. .map_err(|e| format!("failed to read `op` output: {}", e))?;
  157. let status = child
  158. .wait()
  159. .map_err(|e| format!("failed to wait for `op`: {}", e))?;
  160. if !status.success() {
  161. return Err(format!("`op` command exit error: {}", status).into());
  162. }
  163. Ok(buffer)
  164. }
  165. fn search(
  166. &self,
  167. session: &Option<String>,
  168. registry_name: &str,
  169. ) -> Result<Option<String>, Error> {
  170. let cmd = self.make_cmd(
  171. session,
  172. &[
  173. "list",
  174. "items",
  175. "--categories",
  176. "Login",
  177. "--tags",
  178. CARGO_TAG,
  179. ],
  180. );
  181. let buffer = self.run_cmd(cmd)?;
  182. let items: Vec<ListItem> = serde_json::from_str(&buffer)
  183. .map_err(|e| format!("failed to deserialize JSON from 1password list: {}", e))?;
  184. let mut matches = items
  185. .into_iter()
  186. .filter(|item| item.overview.title == registry_name);
  187. match matches.next() {
  188. Some(login) => {
  189. // Should this maybe just sort on `updatedAt` and return the newest one?
  190. if matches.next().is_some() {
  191. return Err(format!(
  192. "too many 1password logins match registry name {}, \
  193. consider deleting the excess entries",
  194. registry_name
  195. )
  196. .into());
  197. }
  198. Ok(Some(login.uuid))
  199. }
  200. None => Ok(None),
  201. }
  202. }
  203. fn modify(&self, session: &Option<String>, uuid: &str, token: &str) -> Result<(), Error> {
  204. let cmd = self.make_cmd(
  205. session,
  206. &["edit", "item", uuid, &format!("password={}", token)],
  207. );
  208. self.run_cmd(cmd)?;
  209. Ok(())
  210. }
  211. fn create(
  212. &self,
  213. session: &Option<String>,
  214. registry_name: &str,
  215. api_url: &str,
  216. token: &str,
  217. ) -> Result<(), Error> {
  218. let cmd = self.make_cmd(
  219. session,
  220. &[
  221. "create",
  222. "item",
  223. "Login",
  224. &format!("password={}", token),
  225. &format!("url={}", api_url),
  226. "--title",
  227. registry_name,
  228. "--tags",
  229. CARGO_TAG,
  230. ],
  231. );
  232. self.run_cmd(cmd)?;
  233. Ok(())
  234. }
  235. fn get_token(&self, session: &Option<String>, uuid: &str) -> Result<String, Error> {
  236. let cmd = self.make_cmd(session, &["get", "item", uuid]);
  237. let buffer = self.run_cmd(cmd)?;
  238. let item: Login = serde_json::from_str(&buffer)
  239. .map_err(|e| format!("failed to deserialize JSON from 1password get: {}", e))?;
  240. let password = item
  241. .details
  242. .fields
  243. .into_iter()
  244. .find(|item| item.designation == "password");
  245. match password {
  246. Some(password) => Ok(password.value),
  247. None => Err("could not find password field".into()),
  248. }
  249. }
  250. fn delete(&self, session: &Option<String>, uuid: &str) -> Result<(), Error> {
  251. let cmd = self.make_cmd(session, &["delete", "item", uuid]);
  252. self.run_cmd(cmd)?;
  253. Ok(())
  254. }
  255. }
  256. impl Credential for OnePasswordKeychain {
  257. fn name(&self) -> &'static str {
  258. env!("CARGO_PKG_NAME")
  259. }
  260. fn get(&self, registry_name: &str, _api_url: &str) -> Result<String, Error> {
  261. let session = self.signin()?;
  262. if let Some(uuid) = self.search(&session, registry_name)? {
  263. self.get_token(&session, &uuid)
  264. } else {
  265. return Err(format!(
  266. "no 1password entry found for registry `{}`, try `cargo login` to add a token",
  267. registry_name
  268. )
  269. .into());
  270. }
  271. }
  272. fn store(&self, registry_name: &str, api_url: &str, token: &str) -> Result<(), Error> {
  273. let session = self.signin()?;
  274. // Check if an item already exists.
  275. if let Some(uuid) = self.search(&session, registry_name)? {
  276. self.modify(&session, &uuid, token)
  277. } else {
  278. self.create(&session, registry_name, api_url, token)
  279. }
  280. }
  281. fn erase(&self, registry_name: &str, _api_url: &str) -> Result<(), Error> {
  282. let session = self.signin()?;
  283. // Check if an item already exists.
  284. if let Some(uuid) = self.search(&session, registry_name)? {
  285. self.delete(&session, &uuid)?;
  286. } else {
  287. eprintln!("not currently logged in to `{}`", registry_name);
  288. }
  289. Ok(())
  290. }
  291. }
  292. fn main() {
  293. let op = match OnePasswordKeychain::new() {
  294. Ok(op) => op,
  295. Err(e) => {
  296. eprintln!("error: {}", e);
  297. std::process::exit(1);
  298. }
  299. };
  300. cargo_credential::main(op);
  301. }