secret_service/blocking/
collection.rs

1use super::item::Item;
2use crate::error::Error;
3use crate::proxy::collection::CollectionProxyBlocking;
4use crate::proxy::service::ServiceProxyBlocking;
5use crate::session::Session;
6use crate::ss::{SS_DBUS_NAME, SS_ITEM_ATTRIBUTES, SS_ITEM_LABEL};
7use crate::util::{exec_prompt_blocking, format_secret, lock_or_unlock_blocking, LockAction};
8
9use std::collections::HashMap;
10use zbus::{
11    proxy::CacheProperties,
12    zvariant::{Dict, ObjectPath, OwnedObjectPath, Value},
13};
14
15// Collection struct.
16// Should always be created from the SecretService entry point,
17// whether through a new collection or a collection search
18pub struct Collection<'a> {
19    conn: zbus::blocking::Connection,
20    session: &'a Session,
21    pub collection_path: OwnedObjectPath,
22    collection_proxy: CollectionProxyBlocking<'a>,
23    service_proxy: &'a ServiceProxyBlocking<'a>,
24}
25
26impl<'a> Collection<'a> {
27    pub(crate) fn new(
28        conn: zbus::blocking::Connection,
29        session: &'a Session,
30        service_proxy: &'a ServiceProxyBlocking,
31        collection_path: OwnedObjectPath,
32    ) -> Result<Self, Error> {
33        let collection_proxy = CollectionProxyBlocking::builder(&conn)
34            .destination(SS_DBUS_NAME)?
35            .path(collection_path.clone())?
36            .cache_properties(CacheProperties::No)
37            .build()?;
38        Ok(Collection {
39            conn,
40            session,
41            collection_path,
42            collection_proxy,
43            service_proxy,
44        })
45    }
46
47    pub fn is_locked(&self) -> Result<bool, Error> {
48        Ok(self.collection_proxy.locked()?)
49    }
50
51    pub fn ensure_unlocked(&self) -> Result<(), Error> {
52        if self.is_locked()? {
53            Err(Error::Locked)
54        } else {
55            Ok(())
56        }
57    }
58
59    pub fn unlock(&self) -> Result<(), Error> {
60        lock_or_unlock_blocking(
61            self.conn.clone(),
62            self.service_proxy,
63            &self.collection_path,
64            LockAction::Unlock,
65        )
66    }
67
68    pub fn lock(&self) -> Result<(), Error> {
69        lock_or_unlock_blocking(
70            self.conn.clone(),
71            self.service_proxy,
72            &self.collection_path,
73            LockAction::Lock,
74        )
75    }
76
77    /// Deletes dbus object, but struct instance still exists (current implementation)
78    pub fn delete(&self) -> Result<(), Error> {
79        // ensure_unlocked handles prompt for unlocking if necessary
80        self.ensure_unlocked()?;
81        let prompt_path = self.collection_proxy.delete()?;
82
83        // "/" means no prompt necessary
84        if prompt_path.as_str() != "/" {
85            exec_prompt_blocking(self.conn.clone(), &prompt_path)?;
86        }
87
88        Ok(())
89    }
90
91    pub fn get_all_items(&'a self) -> Result<Vec<Item<'a>>, Error> {
92        let items = self.collection_proxy.items()?;
93
94        // map array of item paths to Item
95        let res = items
96            .into_iter()
97            .map(|item_path| {
98                Item::new(
99                    self.conn.clone(),
100                    self.session,
101                    self.service_proxy,
102                    item_path.into(),
103                )
104            })
105            .collect::<Result<_, _>>()?;
106
107        Ok(res)
108    }
109
110    pub fn search_items(&'a self, attributes: HashMap<&str, &str>) -> Result<Vec<Item<'a>>, Error> {
111        let items = self.collection_proxy.search_items(attributes)?;
112
113        // map array of item paths to Item
114        let res = items
115            .into_iter()
116            .map(|item_path| {
117                Item::new(
118                    self.conn.clone(),
119                    self.session,
120                    self.service_proxy,
121                    item_path,
122                )
123            })
124            .collect::<Result<_, _>>()?;
125
126        Ok(res)
127    }
128
129    pub fn get_label(&self) -> Result<String, Error> {
130        Ok(self.collection_proxy.label()?)
131    }
132
133    pub fn set_label(&self, new_label: &str) -> Result<(), Error> {
134        Ok(self.collection_proxy.set_label(new_label)?)
135    }
136
137    pub fn create_item(
138        &'a self,
139        label: &str,
140        attributes: HashMap<&str, &str>,
141        secret: &[u8],
142        replace: bool,
143        content_type: &str,
144    ) -> Result<Item<'a>, Error> {
145        let secret_struct = format_secret(self.session, secret, content_type)?;
146
147        let mut properties: HashMap<&str, Value> = HashMap::new();
148        let attributes: Dict = attributes.into();
149
150        properties.insert(SS_ITEM_LABEL, label.into());
151        properties.insert(SS_ITEM_ATTRIBUTES, attributes.into());
152
153        let created_item = self
154            .collection_proxy
155            .create_item(properties, secret_struct, replace)?;
156
157        // This prompt handling is practically identical to create_collection
158        let item_path: ObjectPath = {
159            // Get path of created object
160            let created_path = created_item.item;
161
162            // Check if that path is "/", if so should execute a prompt
163            if created_path.as_str() == "/" {
164                let prompt_path = created_item.prompt;
165
166                // Exec prompt and parse result
167                let prompt_res = exec_prompt_blocking(self.conn.clone(), &prompt_path)?;
168                prompt_res.try_into()?
169            } else {
170                // if not, just return created path
171                created_path.into()
172            }
173        };
174
175        Item::new(
176            self.conn.clone(),
177            self.session,
178            self.service_proxy,
179            item_path.into(),
180        )
181    }
182}
183
184#[cfg(test)]
185mod test {
186    use crate::blocking::*;
187
188    #[test]
189    fn should_create_collection_struct() {
190        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
191        let _ = ss.get_default_collection().unwrap();
192        // tested under SecretService struct
193    }
194
195    #[test]
196    fn should_check_if_collection_locked() {
197        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
198        let collection = ss.get_default_collection().unwrap();
199        let _ = collection.is_locked().unwrap();
200    }
201
202    #[test]
203    #[ignore] // should unignore this test this manually, otherwise will constantly prompt during tests.
204    fn should_lock_and_unlock() {
205        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
206        let collection = ss.get_default_collection().unwrap();
207        let locked = collection.is_locked().unwrap();
208        if locked {
209            collection.unlock().unwrap();
210            collection.ensure_unlocked().unwrap();
211            assert!(!collection.is_locked().unwrap());
212            collection.lock().unwrap();
213            assert!(collection.is_locked().unwrap());
214        } else {
215            collection.lock().unwrap();
216            assert!(collection.is_locked().unwrap());
217            collection.unlock().unwrap();
218            collection.ensure_unlocked().unwrap();
219            assert!(!collection.is_locked().unwrap());
220        }
221    }
222
223    #[test]
224    #[ignore]
225    fn should_delete_collection() {
226        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
227        let collections = ss.get_all_collections().unwrap();
228        let count_before = collections.len();
229        for collection in collections {
230            let collection_path = &*collection.collection_path;
231            if collection_path.contains("Test") {
232                collection.unlock().unwrap();
233                collection.delete().unwrap();
234            }
235        }
236        //double check after
237        let collections = ss.get_all_collections().unwrap();
238        assert!(
239            collections.len() < count_before,
240            "collections before delete {count_before}",
241        )
242    }
243
244    #[test]
245    fn should_get_all_items() {
246        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
247        let collection = ss.get_default_collection().unwrap();
248        collection.get_all_items().unwrap();
249    }
250
251    #[test]
252    fn should_search_items() {
253        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
254        let collection = ss.get_default_collection().unwrap();
255
256        // Create an item
257        let item = collection
258            .create_item(
259                "test",
260                HashMap::from([("test_attributes_in_collection", "test")]),
261                b"test_secret",
262                false,
263                "text/plain",
264            )
265            .unwrap();
266
267        // handle empty vec search
268        collection.search_items(HashMap::new()).unwrap();
269
270        // handle no result
271        let bad_search = collection
272            .search_items(HashMap::from([("test_bad", "test")]))
273            .unwrap();
274        assert_eq!(bad_search.len(), 0);
275
276        // handle correct search for item and compare
277        let search_item = collection
278            .search_items(HashMap::from([("test_attributes_in_collection", "test")]))
279            .unwrap();
280
281        assert_eq!(item.item_path, search_item[0].item_path);
282        item.delete().unwrap();
283    }
284
285    #[test]
286    #[ignore]
287    fn should_get_and_set_collection_label() {
288        let ss = SecretService::connect(EncryptionType::Plain).unwrap();
289        let collection = ss.get_default_collection().unwrap();
290        let label = collection.get_label().unwrap();
291        assert_eq!(label, "Login");
292
293        // Set label to test and check
294        collection.unlock().unwrap();
295        collection.set_label("Test").unwrap();
296        let label = collection.get_label().unwrap();
297        assert_eq!(label, "Test");
298
299        // Reset label to original and test
300        collection.unlock().unwrap();
301        collection.set_label("Login").unwrap();
302        let label = collection.get_label().unwrap();
303        assert_eq!(label, "Login");
304
305        collection.lock().unwrap();
306    }
307
308    #[test]
309    fn should_get_collection_by_path() {
310        let path: OwnedObjectPath;
311        let label: String;
312        // get the default collection on one blocking call, remember its path and label
313        {
314            let ss = SecretService::connect(EncryptionType::Plain).unwrap();
315            let collection = ss.get_default_collection().unwrap();
316            label = collection.get_label().unwrap();
317            path = collection.collection_path.clone();
318        }
319        // get collection by path on another blocking call, check that the label is the same
320        {
321            let ss = SecretService::connect(EncryptionType::Plain).unwrap();
322            let collection_prime = ss.get_collection_by_path(path).unwrap();
323            let label_prime = collection_prime.get_label().unwrap();
324            assert_eq!(label, label_prime);
325        }
326    }
327}