onepassword_cli/
lib.rs

1pub mod error;
2pub mod output;
3mod tests;
4use chrono::{prelude::*, Duration};
5use error::Error;
6
7use std::process::Stdio;
8use tokio::io::AsyncWriteExt;
9use tokio::process::Command;
10
11use crate::sealed::{FirstCmd, SecondCmd};
12
13pub type Result<T> = std::result::Result<T, Error>;
14
15//OpCLI have expiration_time field what is the token's expiration time.
16//Intent to implement some method to auto renew the token. //TODO
17#[derive(Clone)]
18pub struct OpCLI {
19    expiration_time: DateTime<Utc>,
20    session: String,
21}
22
23impl OpCLI {
24    #[inline]
25    pub async fn new_with_pass(username: &str, password: &str) -> Result<Self> {
26        let mut child = Command::new("op")
27            .arg("signin")
28            .arg(username)
29            .arg("--raw")
30            .stdin(Stdio::piped())
31            .stderr(Stdio::piped())
32            .stdout(Stdio::piped())
33            .spawn()?;
34        let stdin = child.stdin.as_mut().unwrap();
35        stdin.write_all(password.as_bytes()).await?;
36        let output = child.wait_with_output().await?;
37        handle_op_signin_error(String::from_utf8_lossy(&output.stderr).to_string()).await?;
38        let expiration_time = Utc::now() + Duration::minutes(29);
39        Ok(Self {
40            expiration_time,
41            session: String::from_utf8_lossy(&output.stdout).to_string(),
42        })
43    }
44
45    pub fn get(&self) -> GetCmd {
46        GetCmd {
47            cmd: "get".to_string(),
48            session: self.session.to_string(),
49        }
50    }
51
52    pub fn create(&self) -> CreateCmd {
53        CreateCmd {
54            cmd: "create".to_string(),
55            session: self.session.to_string(),
56        }
57    }
58
59    #[inline]
60    pub fn list(&self) -> ListCmd {
61        ListCmd {
62            cmd: "list".to_string(),
63            session: self.session.to_string(),
64        }
65    }
66
67    #[inline]
68    pub fn delete(&self) -> DeleteCmd {
69        DeleteCmd {
70            cmd: "delete".to_string(),
71            session: self.session.to_string(),
72        }
73    }
74}
75
76#[derive(Debug, Clone)]
77pub struct GetCmd {
78    cmd: String,
79    session: String,
80}
81
82impl sealed::FirstCmd for GetCmd {
83    #[doc(hidden)]
84    fn cmd(&self) -> &str {
85        &self.cmd
86    }
87    #[doc(hidden)]
88    fn session(&self) -> &str {
89        &self.session
90    }
91}
92
93//this macro repeat codes above. to create a first cmd then
94//implement FirstCmd trait for it.
95macro_rules! its_first_cmd {
96    ($first_cmd:ident) => {
97        #[derive(Debug, Clone)]
98        pub struct $first_cmd {
99            cmd: String,
100            session: String,
101        }
102
103        impl sealed::FirstCmd for $first_cmd {
104            #[doc(hidden)]
105            fn cmd(&self) -> &str {
106                &self.cmd
107            }
108            #[doc(hidden)]
109            fn session(&self) -> &str {
110                &self.session
111            }
112        }
113    };
114}
115
116its_first_cmd!(CreateCmd);
117its_first_cmd!(ListCmd);
118its_first_cmd!(DeleteCmd);
119
120//Maybe I can generic on some of second cmd's method, they seems like do same thing.
121//TODO
122impl GetCmd {
123    pub fn account(&self) -> AccountCmd {
124        let flags: Vec<String> = Vec::new();
125        AccountCmd {
126            first: self.clone(),
127            cmd: "account".to_string(),
128            flags,
129        }
130    }
131
132    ///this method return items' fields of website,username,password
133    pub fn item_lite(&self, item: &str) -> ItemLiteCmd {
134        let flags: Vec<String> = vec![
135            item.to_string(),
136            "--fields".to_string(),
137            "website,username,password".to_string(),
138        ];
139        ItemLiteCmd {
140            first: self.clone(),
141            cmd: "item".to_string(),
142            flags,
143        }
144    }
145
146    pub fn item(&self, item: &str) -> GetItemCmd {
147        let flags: Vec<String> = vec![item.to_string()];
148        GetItemCmd {
149            first: self.clone(),
150            cmd: "item".to_string(),
151            flags,
152        }
153    }
154
155    pub fn document(&self, doc: &str) -> GetDocumentCmd {
156        let flags: Vec<String> = vec![doc.to_string()];
157        GetDocumentCmd {
158            first: self.clone(),
159            cmd: "document".to_string(),
160            flags,
161        }
162    }
163
164    pub fn totp(&self, item_name: &str) -> GetTotpCmd {
165        let flags: Vec<String> = vec![item_name.to_string()];
166
167        GetTotpCmd {
168            first: self.clone(),
169            cmd: "totp".to_string(),
170            flags,
171        }
172    }
173
174    pub fn user(&self, uuid: &str) -> GetUserCmd {
175        let flags: Vec<String> = vec![uuid.to_string()];
176
177        GetUserCmd {
178            first: self.clone(),
179            cmd: "user".to_string(),
180            flags,
181        }
182    }
183}
184impl CreateCmd {
185    pub fn document(&self, path: &str) -> CreateDocumentCmd {
186        let flags: Vec<String> = vec![path.to_string()];
187        CreateDocumentCmd {
188            first: self.clone(),
189            cmd: "document".to_string(),
190            flags,
191        }
192    }
193}
194
195impl ListCmd {
196    pub fn documents(&self) -> ListDocumentsCmd {
197        let flags: Vec<String> = Vec::new();
198        ListDocumentsCmd {
199            first: self.clone(),
200            cmd: "documents".to_string(),
201            flags,
202        }
203    }
204
205    pub fn items(&self) -> ListItemsCmd {
206        let flags: Vec<String> = Vec::new();
207        ListItemsCmd {
208            first: self.clone(),
209            cmd: "items".to_string(),
210            flags,
211        }
212    }
213
214    pub fn users(&self) -> ListUsersCmd {
215        let flags: Vec<String> = Vec::new();
216        ListUsersCmd {
217            first: self.clone(),
218            cmd: "users".to_string(),
219            flags,
220        }
221    }
222}
223
224impl DeleteCmd {
225    pub fn item(&self) -> DeleteItemCmd {
226        let flags: Vec<String> = Vec::new();
227        DeleteItemCmd {
228            first: self.clone(),
229            cmd: "item".to_string(),
230            flags,
231        }
232    }
233
234    pub fn document(&self, doc: &str) -> DeleteDocumentCmd {
235        let flags: Vec<String> = vec![doc.to_string()];
236        DeleteDocumentCmd {
237            first: self.clone(),
238            cmd: "document".to_string(),
239            flags,
240        }
241    }
242}
243
244#[async_trait::async_trait]
245pub trait SecondCmdExt: SecondCmd {
246    fn add_flag(&mut self, flags: &[&str]) -> &Self {
247        for flag in flags {
248            if !self.flags().contains(&flag.to_string()) {
249                self.flags().push(flag.to_string())
250            }
251        }
252        self
253    }
254
255    async fn run(&self) -> Result<Self::Output> {
256        let mut args: Vec<String> = vec![
257            self.first().cmd().to_string(),
258            self.cmd().to_string(),
259            "--session".to_string(),
260            self.first().session().trim().to_string(),
261        ];
262        if !self.flags().is_empty() {
263            self.flags()
264                .into_iter()
265                .for_each(|flag| args.push(flag.to_string()))
266        }
267        let out_str: &str = &exec_command(args).await?;
268        if out_str.len() == 0 {
269            return Ok(serde_json::from_str("{\"field\":\"ok\"}")?);
270        }
271        Ok(serde_json::from_str(out_str)?)
272    }
273}
274
275impl<T: SecondCmd> SecondCmdExt for T {}
276
277#[derive(Debug)]
278pub struct AccountCmd {
279    first: GetCmd,
280    cmd: String,
281    flags: Vec<String>,
282}
283
284#[async_trait::async_trait]
285impl SecondCmd for AccountCmd {
286    type Output = output::Account;
287    type First = GetCmd;
288
289    #[doc(hidden)]
290    fn first(&self) -> &GetCmd {
291        &self.first
292    }
293
294    #[doc(hidden)]
295    fn cmd(&self) -> &str {
296        &self.cmd
297    }
298
299    #[doc(hidden)]
300    fn flags(&self) -> Vec<String> {
301        self.flags.clone()
302    }
303}
304
305//This macro repeat above codes. To create a new second cmd struct
306//and implement SecondCmd trait for it.
307macro_rules! its_second_cmd {
308    ($first_cmd:ident,$second_cmd:ident,$output:ident) => {
309        #[derive(Debug)]
310        pub struct $second_cmd {
311            first: $first_cmd,
312            cmd: String,
313            flags: Vec<String>,
314        }
315
316        #[async_trait::async_trait]
317        impl SecondCmd for $second_cmd {
318            type Output = output::$output;
319            type First = $first_cmd;
320            #[doc(hidden)]
321            fn first(&self) -> &$first_cmd {
322                &self.first
323            }
324            #[doc(hidden)]
325            fn cmd(&self) -> &str {
326                &self.cmd
327            }
328            #[doc(hidden)]
329            fn flags(&self) -> Vec<String> {
330                self.flags.clone()
331            }
332        }
333    };
334}
335
336its_second_cmd!(GetCmd, ItemLiteCmd, ItemLite);
337its_second_cmd!(GetCmd, GetDocumentCmd, Value);
338its_second_cmd!(GetCmd, GetTotpCmd, Value);
339its_second_cmd!(GetCmd, GetItemCmd, GetItem);
340its_second_cmd!(GetCmd, GetUserCmd, GetUser);
341its_second_cmd!(CreateCmd, CreateDocumentCmd, CreateDocument);
342its_second_cmd!(ListCmd, ListDocumentsCmd, ListDocuments);
343its_second_cmd!(ListCmd, ListItemsCmd, ListItems);
344its_second_cmd!(ListCmd, ListUsersCmd, ListUsers);
345its_second_cmd!(DeleteCmd, DeleteItemCmd, DeleteItem);
346its_second_cmd!(DeleteCmd, DeleteDocumentCmd, DeleteDocument);
347
348#[inline]
349async fn exec_command(args: Vec<String>) -> Result<String> {
350    let child = Command::new("op")
351        .args(args)
352        .stdout(Stdio::piped())
353        .stderr(Stdio::piped())
354        .spawn()?;
355    let output = child.wait_with_output().await?;
356    handle_op_exec_error(String::from_utf8_lossy(&output.stderr).to_string()).await?;
357    Ok(String::from_utf8_lossy(&output.stdout).to_string())
358}
359
360#[inline]
361async fn handle_op_signin_error(std_err: String) -> std::result::Result<(), Error> {
362    match std_err.trim() {
363        err if err.contains("401") => Err(Error::OPSignInError("Wrong password".to_string())),
364        err if err.contains("Account not found") => Err(Error::OPSignInError(
365            "Account does not exist,may be you should firstly setup 1password-cli.".to_string(),
366        )),
367        _ => Ok(()),
368    }
369}
370
371#[inline]
372async fn handle_op_exec_error(std_err: String) -> std::result::Result<(), Error> {
373    match std_err.trim() {
374        err if err.contains("doesn't seem to be an item") => {
375            Err(Error::ItemQueryError("Item not founded".to_string()))
376        }
377        err if err.contains("Invalid session token") => {
378            Err(Error::ItemQueryError("In valid session token".to_string()))
379        }
380        err if err.contains("More than one item matches") => Err(Error::ItemQueryError(
381            "More than one item matches,Please specify one by uuid".to_string(),
382        )),
383        _ => Ok(()),
384    }
385}
386
387//why I need this: Cause of SecondCmdExt need explicit scope in.
388//So this will impl a casting method for the struct who
389//implemented SecondCmdExt. now user do not need `use crate::SecondCmdExt`
390macro_rules! impl_casting_method {
391    ($($ObjName:ident),* $(,)?) => {
392        $(
393            impl $ObjName {
394
395                pub async fn run(&self) -> Result<<Self as SecondCmd>::Output> {
396                    <Self as SecondCmdExt>::run(self).await
397                }
398
399                pub fn add_flag(&mut self, flags: &[&str]) -> &Self {
400                    <Self as SecondCmdExt>::add_flag(self, flags)
401                }
402            }
403        )*
404    };
405}
406
407impl_casting_method!(
408    ItemLiteCmd,
409    GetDocumentCmd,
410    GetTotpCmd,
411    GetItemCmd,
412    CreateDocumentCmd,
413    ListDocumentsCmd,
414    ListItemsCmd,
415    AccountCmd
416);
417
418mod sealed {
419    use serde::de::DeserializeOwned;
420
421    pub trait FirstCmd {
422        #[doc(hidden)]
423        fn cmd(&self) -> &str;
424        #[doc(hidden)]
425        fn session(&self) -> &str;
426    }
427
428    #[async_trait::async_trait]
429    pub trait SecondCmd {
430        type Output: DeserializeOwned;
431        type First: FirstCmd + Clone;
432
433        #[doc(hidden)]
434        fn first(&self) -> &Self::First;
435        #[doc(hidden)]
436        fn cmd(&self) -> &str;
437        #[doc(hidden)]
438        fn flags(&self) -> Vec<String>;
439    }
440}