secret_service/blocking/
mod.rs

1//! A blocking secret service API.
2//!
3//! This `SecretService` will block the current thread when making requests to the
4//! secret service server instead of returning futures.
5//!
6//! It is important to not call this these functions in an async context or otherwise the runtime
7//! may stall. See [zbus's blocking documentation] for more details. If you are in an async context,
8//! you should use the [async `SecretService`] instead.
9//!
10//! [zbus's blocking documentation]: https://docs.rs/zbus/latest/zbus/blocking/index.html
11//! [async `SecretService`]: crate::SecretService
12
13use crate::session::Session;
14use crate::ss::SS_COLLECTION_LABEL;
15use crate::util;
16use crate::{proxy::service::ServiceProxyBlocking, util::exec_prompt_blocking};
17use crate::{EncryptionType, Error, SearchItemsResult};
18use std::collections::HashMap;
19use zbus::zvariant::{ObjectPath, OwnedObjectPath, Value};
20
21mod collection;
22pub use collection::Collection;
23mod item;
24pub use item::Item;
25
26/// Secret Service Struct.
27///
28/// This the main entry point for usage of the library.
29///
30/// Creating a new [SecretService] will also initialize dbus
31/// and negotiate a new cryptographic session
32/// ([EncryptionType::Plain] or [EncryptionType::Dh])
33pub struct SecretService<'a> {
34    conn: zbus::blocking::Connection,
35    session: Session,
36    service_proxy: ServiceProxyBlocking<'a>,
37}
38
39impl<'a> SecretService<'a> {
40    /// Create a new [SecretService] instance.
41    ///
42    /// This will initialize its own connection to the session bus.
43    pub fn connect(encryption: EncryptionType) -> Result<SecretService<'a>, Error> {
44        let conn = zbus::blocking::Connection::session().map_err(util::handle_conn_error)?;
45        Self::connect_with_existing(encryption, conn)
46    }
47
48    /// Creates a new [SecretService] instance, utilizing an existing connection handle.
49    ///
50    /// `session_conn` should be connected to the session/user message bus.
51    pub fn connect_with_existing(
52        encryption: EncryptionType,
53        session_conn: zbus::blocking::Connection,
54    ) -> Result<SecretService<'a>, Error> {
55        let service_proxy =
56            ServiceProxyBlocking::new(&session_conn).map_err(util::handle_conn_error)?;
57
58        let session = Session::new_blocking(&service_proxy, encryption)?;
59
60        Ok(SecretService {
61            conn: session_conn,
62            session,
63            service_proxy,
64        })
65    }
66
67    /// Get all collections
68    pub fn get_all_collections(&'a self) -> Result<Vec<Collection<'a>>, Error> {
69        let collections = self.service_proxy.collections()?;
70        collections
71            .into_iter()
72            .map(|object_path| {
73                Collection::new(
74                    self.conn.clone(),
75                    &self.session,
76                    &self.service_proxy,
77                    object_path.into(),
78                )
79            })
80            .collect()
81    }
82
83    /// Get collection by alias.
84    ///
85    /// Most common would be the `default` alias, but there
86    /// is also a specific method for getting the collection
87    /// by default alias.
88    pub fn get_collection_by_alias(&'a self, alias: &str) -> Result<Collection<'a>, Error> {
89        let object_path = self.service_proxy.read_alias(alias)?;
90
91        if object_path.as_str() == "/" {
92            Err(Error::NoResult)
93        } else {
94            Ok(Collection::new(
95                self.conn.clone(),
96                &self.session,
97                &self.service_proxy,
98                object_path,
99            )?)
100        }
101    }
102
103    /// Get default collection.
104    /// (The collection whose alias is `default`)
105    pub fn get_default_collection(&'a self) -> Result<Collection<'a>, Error> {
106        self.get_collection_by_alias("default")
107    }
108
109    /// Get any collection.
110    /// First tries `default` collection, then `session`
111    /// collection, then the first collection when it
112    /// gets all collections.
113    pub fn get_any_collection(&'a self) -> Result<Collection<'a>, Error> {
114        // default first, then session, then first
115
116        self.get_default_collection()
117            .or_else(|_| self.get_collection_by_alias("session"))
118            .or_else(|_| {
119                let mut collections = self.get_all_collections()?;
120                if collections.is_empty() {
121                    Err(Error::NoResult)
122                } else {
123                    Ok(collections.swap_remove(0))
124                }
125            })
126    }
127
128    /// Creates a new collection with a label and an alias.
129    pub fn create_collection(&'a self, label: &str, alias: &str) -> Result<Collection<'a>, Error> {
130        let mut properties: HashMap<&str, Value> = HashMap::new();
131        properties.insert(SS_COLLECTION_LABEL, label.into());
132
133        let created_collection = self.service_proxy.create_collection(properties, alias)?;
134
135        // This prompt handling is practically identical to create_collection
136        let collection_path: ObjectPath = {
137            // Get path of created object
138            let created_path = created_collection.collection;
139
140            // Check if that path is "/", if so should execute a prompt
141            if created_path.as_str() == "/" {
142                let prompt_path = created_collection.prompt;
143
144                // Exec prompt and parse result
145                let prompt_res = exec_prompt_blocking(self.conn.clone(), &prompt_path)?;
146                prompt_res.try_into()?
147            } else {
148                // if not, just return created path
149                created_path.into()
150            }
151        };
152
153        Collection::new(
154            self.conn.clone(),
155            &self.session,
156            &self.service_proxy,
157            collection_path.into(),
158        )
159    }
160
161    /// Searches all items by attributes
162    pub fn search_items(
163        &'a self,
164        attributes: HashMap<&str, &str>,
165    ) -> Result<SearchItemsResult<Item<'a>>, Error> {
166        let items = self.service_proxy.search_items(attributes)?;
167
168        let object_paths_to_items = |items: Vec<_>| {
169            items
170                .into_iter()
171                .map(|item_path| {
172                    Item::new(
173                        self.conn.clone(),
174                        &self.session,
175                        &self.service_proxy,
176                        item_path,
177                    )
178                })
179                .collect::<Result<_, _>>()
180        };
181
182        Ok(SearchItemsResult {
183            unlocked: object_paths_to_items(items.unlocked)?,
184            locked: object_paths_to_items(items.locked)?,
185        })
186    }
187
188    /// Unlock all items in a batch
189    pub fn unlock_all(&'a self, items: &[&Item]) -> Result<(), Error> {
190        let objects = items.iter().map(|i| &*i.item_path).collect();
191        let lock_action_res = self.service_proxy.unlock(objects)?;
192
193        if lock_action_res.object_paths.is_empty() {
194            exec_prompt_blocking(self.conn.clone(), &lock_action_res.prompt)?;
195        }
196
197        Ok(())
198    }
199
200    pub fn get_item_by_path(&'a self, item_path: OwnedObjectPath) -> Result<Item<'a>, Error> {
201        Item::new(
202            self.conn.clone(),
203            &self.session,
204            &self.service_proxy,
205            item_path,
206        )
207    }
208
209    pub fn get_collection_by_path(
210        &'a self,
211        collection_path: OwnedObjectPath,
212    ) -> Result<Collection<'a>, Error> {
213        Collection::new(
214            self.conn.clone(),
215            &self.session,
216            &self.service_proxy,
217            collection_path,
218        )
219    }
220}
221
222#[cfg(test)]
223mod test {
224    use super::*;
225    use std::convert::TryFrom;
226    use zbus::zvariant::ObjectPath;
227
228    #[test]
229    fn should_create_secret_service() {
230        SecretService::connect(EncryptionType::Plain).unwrap();
231    }
232
233    #[test]
234    fn should_get_all_collections() {
235        // Assumes that there will always be a default
236        // collection
237        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
238        let collections = ss.get_all_collections().unwrap();
239        assert!(!collections.is_empty(), "no collections found");
240    }
241
242    #[test]
243    fn should_get_collection_by_alias() {
244        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
245        ss.get_collection_by_alias("session").unwrap();
246    }
247
248    #[test]
249    fn should_return_error_if_collection_doesnt_exist() {
250        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
251
252        match ss.get_collection_by_alias("definitely_definitely_does_not_exist") {
253            Err(Error::NoResult) => {}
254            _ => panic!(),
255        };
256    }
257
258    #[test]
259    fn should_get_default_collection() {
260        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
261        ss.get_default_collection().unwrap();
262    }
263
264    #[test]
265    fn should_get_any_collection() {
266        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
267        let _ = ss.get_any_collection().unwrap();
268    }
269
270    #[test_with::no_env(GITHUB_ACTIONS)]
271    #[test]
272    fn should_create_and_delete_collection() {
273        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
274        let test_collection = ss.create_collection("Test", "").unwrap();
275        assert_eq!(
276            ObjectPath::from(test_collection.collection_path.clone()),
277            ObjectPath::try_from("/org/freedesktop/secrets/collection/Test").unwrap()
278        );
279        test_collection.delete().unwrap();
280    }
281
282    #[test]
283    fn should_search_items() {
284        let ss = SecretService::connect(EncryptionType::Dh).unwrap();
285        let collection = ss.get_default_collection().unwrap();
286
287        // Create an item
288        let item = collection
289            .create_item(
290                "test",
291                HashMap::from([("test_attribute_in_ss", "test_value")]),
292                b"test_secret",
293                false,
294                "text/plain",
295            )
296            .unwrap();
297
298        // handle empty vec search
299        ss.search_items(HashMap::new()).unwrap();
300
301        // handle no result
302        let bad_search = ss.search_items(HashMap::from([("test", "test")])).unwrap();
303        assert_eq!(bad_search.unlocked.len(), 0);
304        assert_eq!(bad_search.locked.len(), 0);
305
306        // handle correct search for item and compare
307        let search_item = ss
308            .search_items(HashMap::from([("test_attribute_in_ss", "test_value")]))
309            .unwrap();
310
311        assert_eq!(item.item_path, search_item.unlocked[0].item_path);
312        assert_eq!(search_item.locked.len(), 0);
313        item.delete().unwrap();
314    }
315}